nowbar_flutter 0.2.0
nowbar_flutter: ^0.2.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 #
Status: 0.2.x (pre-1.0). The package is published on pub.dev. The public API described below is frozen for the 0.2 line; the surface widgets are demo components, not live system integrations.
This repository holds two things: the nowbar_flutter package and the Android
16 Live Update timer app under example/. nowbar_flutter is a
pure-Dart Flutter recreation of the Samsung Now Bar, a compact, draggable card
deck that stacks animated surfaces for media, notifications, routines, sports,
and timers. The package documentation is the main body of this file, and the
example app has its own section right below.
Android 16 Live Update timer #
The app under example/ is a countdown timer that drives an Android
16 Live Update, a promoted ongoing notification. Set a duration with the 1, 3,
5, 10, 15, 30, or 60-minute presets, or fine-tune it with the −1분 / +1분
steppers (1 minute to 4 hours), then press 시작 to start and 종료 to stop.
While the countdown runs, it appears in three places: the notification shade's "Live info" (실시간 정보) section, a lock-screen Now Bar card, and a status-bar chip.
[Timer app with a running countdown and a status-bar chip] [Lock screen Now Bar card showing the running timer] [Notification shade Live info section showing the timer card]
The example app owns the integration through its own nowbar/live_timer
method channel. start takes {seconds, totalSeconds}, and stop ends the
update. Live Updates require Android 16 or newer, and the example manifest
declares POST_NOTIFICATIONS and POST_PROMOTED_NOTIFICATIONS.
To run it:
cd example
flutter run
See example/README.md for the channel contract. This
Live Update integration lives in the example app only and does not change
the package's presentation-only scope.
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.10.0 |
| Dart SDK | ^3.0.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 #
The package is published on pub.dev:
flutter pub add nowbar_flutter
Or add it to pubspec.yaml directly:
dependencies:
nowbar_flutter: ^0.1.1
You can also depend on the repository directly:
# 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 public, so the git dependency is reachable directly.
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
The example is a countdown timer app, not a Now Bar demo. It no longer imports
the package's widgets or uses the card deck; instead it exercises the package's
Material 3 theme layer and drives an Android 16 Live Update (a promoted ongoing
notification) through a nowbar/live_timer platform channel declared by the
example itself. Set a duration from the minute presets (1, 3, 5, 10, 15, 30, 60)
or the −1분 / +1분 steppers (1 minute to 4 hours), then start the countdown.
On Android 16 or newer the running timer appears in three places:
- the notification shade's "Live info" (실시간 정보) section,
- a lock-screen Now Bar card, and
- a status-bar chip.
This Live Update integration lives in the example app only. It does not
change the package's scope: nowbar_flutter still ships no notification code,
no platform channels, and no native code. The example declares
POST_NOTIFICATIONS and POST_PROMOTED_NOTIFICATIONS in its manifest, and Live
Updates require Android 16 or newer. See example/README.md
for the channel contract, setup, and the golden-image workflow.
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 #
- Inspired by the Jetpack Compose project
styropyr0/NowBar. This package is an independent reimplementation, and it is not affiliated with, sponsored by, or endorsed by Samsung or the original author. It ships no code or artwork from the original repository, and a license request to the upstream author is in progress. - Inter typeface by Rasmus Andersson.
- Built with Flutter.