nowbar_flutter 0.1.0
nowbar_flutter: ^0.1.0 copied to clipboard
A pure-Dart Flutter recreation of the Samsung Now Bar with animated media, notification, routine, sports, and timer surfaces.
English | 한국어
nowbar_flutter #
A pure-Dart Flutter recreation of the Samsung Now Bar: a compact, draggable card deck that sits at the bottom of your app and stacks animated surfaces for media, notifications, routines, sports, and timers.
Inspired by the Jetpack Compose project
styropyr0/NowBar. This package is an independent reimplementation inspired by the original Jetpack Compose project. It is not affiliated with, sponsored by, or endorsed by Samsung or the original author, and it ships no code or artwork from the original repository. A license request to the upstream author is in progress.
Status: pre-release (0.1.0). The package is not on pub.dev yet. The public API described below is frozen for the first release; the surface widgets are demo components, not live system integrations (see Scope).
[The Now Bar deck with the media surface on top] [The routines surface after a swipe-to-dismiss]
Scope (this is a UI recreation) #
nowbar_flutter recreates the look and motion of the Now Bar. It is a
presentation package only:
- No real media playback, no background audio, no platform media session.
- No integration with system notifications or Android notification APIs.
- No background execution, geolocation, or health/fitness data access.
The five built-in widgets are demo surfaces driven by the values you pass in. Wire them to your own state, controllers, and platform services as needed.
Features #
- Draggable, stacked card deck. Components cycle through a bottom-aligned surface with configurable drag directions.
- Five built-in demo widgets: media player, notification, routines, sports, and timer.
- Pure Dart, zero native code. No platform channels, no plugins, no
MethodChannel; Android is the only supported target. - Full layout control through immutable
NowBarMetrics(corner radius, height, translation clamp, shadow, width fraction, animation speed). - Theming. Material 3 color schemes ported from the reference design, plus
seedColorsupport for a custom palette, in light and dark. - Bundled Inter typeface (Regular 400, SemiBold 600, Bold 700), no font download required.
- Swappable component surfaces via
NowBarComponent.builder, so you can drop in any widget you like.
Requirements #
| Requirement | Version |
|---|---|
| Flutter | >= 3.44.0 |
| Dart SDK | ^3.12.0 |
| Platform | Android |
The package is pure Dart. It declares no platform plugins and calls no native code, so it needs no platform-specific setup on Android.
Installation #
Once the package is published on pub.dev:
flutter pub add nowbar_flutter
Or add it to pubspec.yaml directly:
dependencies:
nowbar_flutter: ^0.1.0
Until the first release lands, depend on the repository:
# Path dependency (local checkout)
dependencies:
nowbar_flutter:
path: ../nowbar_flutter
# Git dependency
dependencies:
nowbar_flutter:
git:
url: https://github.com/winwin-ai/nowbar_flutter.git
ref: main
The repository is currently private; the git dependency assumes it has been made public and is reachable with your credentials.
Quick start #
The example below is complete and runnable. It installs the Now Bar theme, places the bar at the bottom of the screen, and registers three components.
import 'package:flutter/material.dart';
import 'package:nowbar_flutter/nowbar_flutter.dart';
void main() => runApp(const NowBarExampleApp());
class NowBarExampleApp extends StatelessWidget {
const NowBarExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Now Bar Demo',
debugShowCheckedModeBanner: false,
home: NowBarTheme(
darkTheme: true,
child: Scaffold(
backgroundColor: Colors.black,
body: SafeArea(
child: Align(
alignment: Alignment.bottomCenter,
child: NowBarWidget(
innerPadding: const EdgeInsets.fromLTRB(16, 0, 16, 24),
dragDirection: NowBarDragController.dragVertically,
metrics: const NowBarMetrics(
cornerRadius: 32,
widgetHeight: 88,
),
widgets: [
NowBarComponent(
builder: () => MediaPlayerWidget(
tracks: [
MediaPlayerTrack(
image: const NetworkImage(
'https://picsum.photos/seed/nowbar/200',
),
title: 'Midnight City',
artist: 'M83',
),
],
),
),
NowBarComponent(
builder: () => const NotificationWidget(
icon: Icon(NowBarIcons.check),
title: 'Order shipped',
content: 'Your package is on the way',
),
),
NowBarComponent(
builder: () => const RoutinesWidget(content: 'Run 5 km'),
dismissible: false,
),
],
),
),
),
),
),
);
}
}
Notes:
NowBarComponenttakes aWidget Function(), so pass a closure (builder: () => ...). Function literals are not constants, so thewidgetslist itself cannot beconst.- Replace the
NetworkImagewith anAssetImage,FileImage, or any otherImageProviderfor your own artwork.
API reference #
All types below are exported from package:nowbar_flutter/nowbar_flutter.dart.
NowBarWidget #
The root surface that hosts and rotates the active component.
NowBarWidget({
Key? key,
EdgeInsets innerPadding = EdgeInsets.zero,
required List<NowBarComponent> widgets,
NowBarMetrics metrics = const NowBarMetrics(),
NowBarDragController dragDirection = NowBarDragController.dragUp,
})
| Parameter | Type | Default | Description |
|---|---|---|---|
innerPadding |
EdgeInsets |
EdgeInsets.zero |
Padding applied inside the bar surface. |
widgets |
List<NowBarComponent> |
required | The components in the rotation, in display order. |
metrics |
NowBarMetrics |
const NowBarMetrics() |
Layout and motion metrics. |
dragDirection |
NowBarDragController |
NowBarDragController.dragUp |
Gesture direction the surface responds to. |
NowBarComponent #
One entry in the rotation: a builder for the surface content and a dismissal flag.
NowBarComponent({
required Widget Function() builder,
bool dismissible = true,
})
| Parameter | Type | Default | Description |
|---|---|---|---|
builder |
Widget Function() |
required | Builds the widget shown while this component is active. |
dismissible |
bool |
true |
Whether a swipe can remove this component from the rotation. |
Equality is identity-based: each component owns a distinct builder, and the rotation tracks entries by object identity.
NowBarMetrics #
Immutable layout and animation metrics. Every field defaults to the reference design value.
const NowBarMetrics({
double cornerRadius = 50,
double widgetHeight = 80,
(double, double) translationClamp = (-200, 250),
double shadowElevation = 12,
double fillMaxWidthOffset = 0.9,
int animationMultiplier = 1,
})
| Field | Type | Default | Description |
|---|---|---|---|
cornerRadius |
double |
50 |
Corner radius of the bar surface, in logical pixels. |
widgetHeight |
double |
80 |
Height of a single surface, in logical pixels. |
translationClamp |
(double, double) |
(-200, 250) |
Lower and upper bounds for vertical drag translation, in physical pixels. The first value clamps upward movement, the second downward. |
shadowElevation |
double |
12 |
Elevation of the bar's shadow, in logical pixels. |
fillMaxWidthOffset |
double |
0.9 |
Fraction (0 to 1) of the available width the bar occupies when maximized. |
animationMultiplier |
int |
1 |
Integer multiplier for animation durations. Values above 1 slow motion down, below 1 speed it up. |
Units. The vertical translation, its clamp, the stack offsets, and the drag thresholds are physical pixels, matching the reference implementation, so vertical travel and card stacking behave the same as the original across display densities. The corner radius, height, and shadow are logical pixels.
Use copyWith to derive a variant without mutating the original:
const base = NowBarMetrics();
final compact = base.copyWith(widgetHeight: 64, cornerRadius: 24);
NowBarDragController #
The gesture direction a surface responds to.
| Value | Description |
|---|---|
dragUp |
Only upward drags advance to the next component. |
dragDown |
Only downward drags step back to the previous component. |
dragVertically |
Both upward and downward drags step through components. |
dragHorizontally |
No vertical cycling only; horizontal swipe-to-dismiss is still available when the active component is dismissible (see below). |
NotificationColorController #
An immutable palette for a notification surface: background (solid or
gradient), icon, title, and content colors. Despite the name, it is a value
object, not a live controller. Use copyWith to derive a variant.
const NotificationColorController({
Color backgroundColor = Color(0xFF303164),
Gradient? backgroundColorGradient,
Color iconColor = Colors.white,
Color titleColor = Colors.white,
Color contentColor = Colors.white,
})
| Field | Type | Default | Description |
|---|---|---|---|
backgroundColor |
Color |
Color(0xFF303164) |
Solid background, used when backgroundColorGradient is null. |
backgroundColorGradient |
Gradient? |
null |
Optional gradient; when set it takes precedence over backgroundColor. |
iconColor |
Color |
Colors.white |
Color of the leading icon. |
titleColor |
Color |
Colors.white |
Color of the title. |
contentColor |
Color |
Colors.white |
Color of the body text. |
NowBarTheme #
Wraps child in a Material theme configured with the ported Now Bar color
scheme and typography, so descendants can read colors and text styles through
Theme.of(context).
const NowBarTheme({
Key? key,
required Widget child,
bool darkTheme = false,
Color? seedColor,
})
| Parameter | Type | Default | Description |
|---|---|---|---|
child |
Widget |
required | The subtree the theme applies to. |
darkTheme |
bool |
false |
Selects the dark scheme. false applies the light scheme. |
seedColor |
Color? |
null |
Generates a custom Material 3 scheme from this color. When null, the ported Now Bar palette is used. |
Named constructors:
| Constructor | Description |
|---|---|
NowBarTheme.light({...}) |
Always uses the light scheme. |
NowBarTheme.dark({...}) |
Always uses the dark scheme. |
Static builder:
static ThemeData buildThemeData({bool dark = false, Color? seedColor})
buildThemeData returns the ThemeData the wrapper applies. It is named
buildThemeData rather than build because Dart forbids a static member from
sharing the inherited instance build method's name. Use it when you need to
compose the theme into your own MaterialApp:
MaterialApp(
theme: NowBarTheme.buildThemeData(),
darkTheme: NowBarTheme.buildThemeData(dark: true),
home: const HomePage(),
)
NowBarColors #
Color seeds and the two Material 3 color schemes used by Now Bar surfaces.
| Member | Type | Value |
|---|---|---|
purple80 |
Color |
Color(0xFFD0BCFF) |
purpleGrey80 |
Color |
Color(0xFFCCC2DC) |
pink80 |
Color |
Color(0xFFEFB8C8) |
purple40 |
Color |
Color(0xFF6650A4) |
purpleGrey40 |
Color |
Color(0xFF625B71) |
pink40 |
Color |
Color(0xFF7D5260) |
lightColorScheme |
ColorScheme |
Material 3 light scheme with the reference primary/secondary/tertiary seeds pinned. |
darkColorScheme |
ColorScheme |
Material 3 dark scheme with the reference seeds pinned. |
Android 12+ dynamic color from the reference theme is intentionally omitted: wallpaper-derived color has no portable Flutter equivalent, so these schemes are stable across Android devices.
NowBarTypography #
Typography used by Now Bar surfaces. The three overridden styles preserve the
reference metrics, deriving Flutter's TextStyle.height from the reference's
absolute line height.
| Member | Type | Description |
|---|---|---|
fontFamily |
const String |
The bundled family name, 'Inter'. |
bodyLarge |
TextStyle |
Inter Regular, 16px, 24px line box, 0.5 letter spacing. |
titleLarge |
TextStyle |
Inter Bold, 22px, 28px line box, no letter spacing. |
labelSmall |
TextStyle |
Inter SemiBold, 11px, 16px line box, 0.5 letter spacing. |
textTheme |
TextTheme |
The full Material 3 text theme with Inter applied and the three styles above overridden. |
NowBarIcons #
A neutral icon catalog mapped to the closest Material icons. The package ships no branded artwork.
| Member | IconData |
|---|---|
play |
Icons.play_arrow |
pause |
Icons.pause |
skipNext |
Icons.skip_next |
skipPrevious |
Icons.skip_previous |
restart |
Icons.replay |
timer |
Icons.timer |
check |
Icons.check |
share |
Icons.share |
Built-in widgets #
Each widget is a demo surface. Drop one into a NowBarComponent.builder and
supply its data yourself.
MediaPlayerWidget #
Presents a media track with transport controls.
const MediaPlayerWidget({
Key? key,
required List<MediaPlayerTrack> tracks,
EdgeInsets innerPadding = EdgeInsets.zero,
})
| Parameter | Type | Default | Description |
|---|---|---|---|
tracks |
List<MediaPlayerTrack> |
required | The queue of tracks to surface. |
innerPadding |
EdgeInsets |
EdgeInsets.zero |
Padding applied inside the surface. |
MediaPlayerTrack
const MediaPlayerTrack({
required ImageProvider image,
required String title,
required String artist,
})
| Field | Type | Description |
|---|---|---|
image |
ImageProvider |
Cover artwork. Any ImageProvider works (AssetImage, NetworkImage, FileImage, ...). |
title |
String |
Track title. |
artist |
String |
Track artist. |
NotificationWidget #
Presents an ongoing or recent notification.
const NotificationWidget({
Key? key,
required Widget icon,
required String title,
required String content,
NotificationColorController colorController =
const NotificationColorController(),
})
| Parameter | Type | Default | Description |
|---|---|---|---|
icon |
Widget |
required | Leading icon, typically an Icon. |
title |
String |
required | Notification title. |
content |
String |
required | Notification body. |
colorController |
NotificationColorController |
const NotificationColorController() |
Colors and optional gradient for the surface. |
RoutinesWidget #
Presents a fitness routine or activity session.
const RoutinesWidget({
Key? key,
String title = 'Routines',
required String content,
})
| Parameter | Type | Default | Description |
|---|---|---|---|
title |
String |
'Routines' |
Heading text. |
content |
String |
required | Routine description or progress text. |
SportsWidget #
Presents a live sports score.
const SportsWidget({
Key? key,
required String title,
required String content,
ImageProvider? backgroundImage,
Widget? leading,
Widget? trailing,
})
| Parameter | Type | Default | Description |
|---|---|---|---|
title |
String |
required | Match or event title. |
content |
String |
required | Score or status text. |
backgroundImage |
ImageProvider? |
null |
Optional background artwork. |
leading |
Widget? |
null |
Optional leading widget, such as a team badge. |
trailing |
Widget? |
null |
Optional trailing widget, such as an action. |
TimerWidget #
Presents a countdown timer or stopwatch.
const TimerWidget({
Key? key,
required int seconds,
})
| Parameter | Type | Default | Description |
|---|---|---|---|
seconds |
int |
required | Remaining or elapsed time, in seconds. |
Customization #
Metrics #
NowBarMetrics controls geometry and motion. Override only what you need with
copyWith, or construct a full set:
const NowBarMetrics(
cornerRadius: 24, // rounder or squarer surfaces
widgetHeight: 72, // taller or shorter bar
translationClamp: (-120, 180), // physical px; limit how far the bar can travel
shadowElevation: 8, // lighter shadow
fillMaxWidthOffset: 0.85, // fraction of available width
animationMultiplier: 2, // slower, more deliberate motion
)
| Knob | Effect |
|---|---|
cornerRadius |
Surface roundness. |
widgetHeight |
Surface height. |
translationClamp |
(lower, upper) bounds for drag translation, in physical pixels; the first value clamps upward movement, the second downward. |
shadowElevation |
Shadow depth. |
fillMaxWidthOffset |
Width as a fraction of the available space (for example 0.9 = 90%). |
animationMultiplier |
Global animation speed. > 1 slows down, < 1 speeds up. |
Drag directions #
Pick the gesture axis to match your UX:
NowBarWidget(
dragDirection: NowBarDragController.dragVertically,
widgets: [...],
)
Use dragVertically to browse the rotation. Horizontal swipe-to-dismiss is
independent of dragDirection: it is always available for the active component
whose dismissible flag is true, whatever the chosen value.
Notification colors and gradients #
const NotificationWidget(
icon: Icon(NowBarIcons.check),
title: 'Delivered',
content: 'Left at the front door',
colorController: NotificationColorController(
backgroundColorGradient: LinearGradient(
colors: [Color(0xFF6A5AE0), Color(0xFF303164)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
iconColor: Colors.amber,
titleColor: Colors.white,
contentColor: Colors.white70,
),
)
When backgroundColorGradient is set it takes precedence over
backgroundColor. Because copyWith cannot distinguish an omitted gradient
from a null one, use the constructor when you need to clear a gradient.
Theme and seed color #
Wrap the subtree that renders Now Bar content. Use the ported palette, or generate a custom Material 3 scheme from a single color:
MaterialApp(
home: NowBarTheme(
darkTheme: true,
seedColor: const Color(0xFF00BFA5),
child: const HomePage(),
),
)
Drop seedColor to keep the ported Now Bar palette. Drop darkTheme (or use
NowBarTheme.light) for the light scheme. NowBarTheme wraps its child with
a Theme, so place it below MaterialApp; a wrapper above MaterialApp
would be shadowed by the app's own theme. To theme the whole app instead, call
NowBarTheme.buildThemeData() and pass the result to theme / darkTheme.
Platform notes #
- Supported platform: Android. Android is the only supported and tested target. Other Flutter targets (iOS, macOS, Linux, Windows, and web) are unsupported and untested, and the package does not guarantee that they render or behave correctly.
- No platform channels. The package contains no platform channels, so there
is nothing to configure in native manifests such as
AndroidManifest.xml.
Example app #
A runnable app lives under example/. It depends on the package by
path, so run it from a checkout:
cd example
flutter run
Testing #
Run the package test suite from the repository root:
flutter test
The suite runs 116 unit and widget tests plus 10 golden-image tests. The golden
tests carry the golden tag, and CI runs flutter test --exclude-tags golden
because golden images are platform-dependent. Regenerate them on macOS from the
package root, and again inside example/ for the example app's golden:
flutter test --update-goldens --tags golden
(cd example && flutter test --update-goldens --tags golden)
The suite loads the bundled Inter fonts through FontLoader, so typography
changes are covered without a device or emulator.
Contributing #
Issues and pull requests are welcome on GitHub. Before opening a PR:
- Run
dart format .,flutter analyze, andflutter testand make sure all three pass. - Keep changes focused and add tests for behavior changes.
- Match the existing code style and public API naming.
License #
Released under the MIT License. See LICENSE.
Third-party notices #
This package bundles the Inter typeface (Regular 400, SemiBold 600, Bold
700) from https://github.com/rsms/inter, licensed under the SIL Open Font
License, Version 1.1. The font files are distributed unmodified; the
reserved name "Inter" identifies the unmodified upstream font. The full license
text is bundled at assets/fonts/OFL.txt. See
THIRD_PARTY_NOTICES.md for details.
Credits #
- Design language inspired by the Jetpack Compose project
styropyr0/NowBar. - Inter typeface by Rasmus Andersson.
- Built with Flutter.