FlutterNotificationQueue
A production-grade, overlay-based spatial notification engine for Flutter applications.
FlutterNotificationQueue (FNQ) provides a comprehensive solution for displaying in-app notifications with spatial queue placement, Intent-First interactive drag/reorder/relocate gestures, notification bundling, priority triage preemption, dynamic channel parking, real-time observability, and deep theming.
💡 Core Architecture & Mental Model
FNQ cleanly decouples what a message represents from where and how it is rendered on screen:
┌────────────────────────┐ ┌─────────────────────────┐
│ AppNotification │ │ NotificationChannel │
│ (Data Payload Intent) │────► │ (Visual Category/Theme)│
└────────────────────────┘ └────────────┬────────────┘
│ Maps route position
▼
┌────────────────────────┐ ┌─────────────────────────┐
│ NotificationController │ │ NotificationQueue │
│ & NotificationScope │ ◄──► │ (Spatial Position/Rules)│
│ (Lifecycle & State) │ │ (Gestures & Grouping) │
└────────────────────────┘ └─────────────────────────┘
AppNotification: Pure data payload describing notification content, title, message, priority, dismiss duration, actions, and tap behaviors.NotificationChannel: Logical grouping for messages (e.g.,success,error,chat). Governs default colors, icons, priorities, auto-dismiss durations, and target queue positions.NotificationQueue: Spatial layout container fixed to aQueuePosition(e.g.,topRight,bottomCenter). Controls physics, max stack limits, overflow strategies, drag behaviors (Dismiss,Reorder,Relocate,ReorderAndRelocate), and grouping decks.NotificationController&NotificationScope: State management handle and widget-tree dependency injector. Manages lifecycle, contextless or context-aware dispatching, event streams, history logging, and live runtime reconfiguration.
⚡ Quick Start
1. Add Dependency
Add flutter_notification_queue to your pubspec.yaml:
dependencies:
flutter_notification_queue: ^0.4.0
2. Initialize and Mount Scope
Initialize a NotificationController and wrap your MaterialApp with NotificationScope:
import 'package:flutter/material.dart';
import 'package:flutter_notification_queue/flutter_notification_queue.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
// 1. Instantiate the controller with queues and channels
final controller = NotificationController(
channels: {
const NotificationChannel(
name: 'info',
position: QueuePosition.topRight,
defaultColor: Color(0xFF0EA5E9),
),
const NotificationChannel(
name: 'error',
position: QueuePosition.topRight,
defaultColor: Color(0xFFEF4444),
),
},
queues: {
const NotificationQueue(
position: QueuePosition.topRight,
maxStackSize: 3,
style: FilledQueueStyle(
borderRadius: BorderRadius.all(Radius.circular(12)),
elevation: 6,
),
),
},
);
return MaterialApp(
// 2. Attach NotificationScope at the root builder
builder: (context, child) => NotificationScope(
controller: controller,
child: child!,
),
home: const HomeScreen(),
);
}
}
3. Dispatch Notifications
Show notifications from UI widgets using NotificationScope.of(context) or contextlessly from BLoCs, Cubits, Repositories, or Services using controller.show():
// Context-aware dispatch from UI
NotificationScope.of(context).show(
const AppNotification(
title: 'Update Complete',
message: 'System package updated successfully.',
channelName: 'info',
),
);
// Error notification with button action
NotificationScope.of(context).show(
AppNotification(
title: 'Connection Failure',
message: 'Unable to reach backend service. Retrying...',
channelName: 'error',
dismissDuration: null, // Stay until dismissed
action: NotificationAction.button(
label: 'RETRY',
onPressed: () => print('Retry clicked'),
),
),
);
// Contextless dispatch (from BLoCs, Cubits, Repositories, or background services)
controller.show(
const AppNotification(
title: 'Background Sync',
message: 'Data synchronization completed in background.',
channelName: 'info',
),
);
// Safe contextless dispatch before first frame renders
controller.tryShow(
const AppNotification(
title: 'App Startup',
message: 'Loaded user session.',
channelName: 'info',
),
);
🎨 Common Recipes & Use Cases
Custom Actions & Tap Behaviors
Configure how users interact with the card surface or action buttons:
// Button Action
AppNotification(
title: 'New Message',
message: 'Sarah sent you a message.',
channelName: 'info',
action: NotificationAction.button(
label: 'REPLY',
onPressed: () => openChat(),
),
);
// Tap-To-Act (Entire card surface is tapable)
AppNotification(
title: 'Security Alert',
message: 'Unrecognized login attempt. Tap to review.',
channelName: 'warning',
tapBehavior: TapToAct(
onTap: () => openSecuritySettings(),
dismissOnAct: true,
),
);
// Tap-To-Expand (Expand long text on tap)
const AppNotification(
title: 'Build Output',
message: 'Detailed build log output spanning multiple lines...',
channelName: 'info',
tapBehavior: TapToExpand(),
);
Permanent & Sticky Notifications
// Permanent card (no auto-dismiss countdown)
const AppNotification(
title: 'Ongoing Download',
message: 'Downloading asset pack (45%)...',
channelName: 'info',
dismissDuration: null,
);
// Sticky Alert (Disables swipe gestures for this specific card)
AppNotification(
title: 'Critical Alert',
message: 'System requires immediate attention.',
channelName: 'error',
dismissDuration: null,
dragBehavior: const Disabled(),
);
🚀 Advanced Capabilities
1. Intent-First Interactive Gestures
FNQ provides sophisticated pointer and touch gesture handling configured at the queue level via dragBehavior and longPressDragBehavior:
Dismiss: Swipe to dismiss. SupportsDismissZone.sideEdgesorDismissZone.naturalDirection.Reorder: Live-shifting drag reordering within the stack featuring Gravity-Well Hysteresis to eliminate pointer wobble and Selection Reticles for drop slot feedback.Relocate: Drag a notification card to move it into another queue position.ReorderAndRelocate: Reorder within the current stack, or drag past an escape threshold to relocate to a destination queue.Disabled: Disables drag gestures.
NotificationQueue(
position: QueuePosition.topRight,
dragBehavior: const Dismiss(),
longPressDragBehavior: ReorderAndRelocate.to(
positions: {QueuePosition.bottomRight},
),
)
Tip
Relocation Auto-Registration: When target positions are defined in Relocate.to({...}) or ReorderAndRelocate.to(positions: {...}), FNQ automatically registers sibling target queues, clones source queue styling, and configures self-inclusion so cards can return home.
2. Notification Grouping (Bundling)
Prevent screen flooding by collapsing multiple notifications from the same channel/group into an interactive stacked deck:
NotificationQueue(
position: QueuePosition.topRight,
groupingBehavior: QueueGroupingBehavior(
enabled: true,
maxBeforeGrouping: 2, // Bundle after 2 notifications arrive
maxStackedLayers: 3, // Background card layers in bundle deck
stackStepOffset: 6.0, // Pixels offset per background card
stackScaleMultiplier: 0.04, // Scale reduction per background card
enableGroupSwipeDismiss: true,// Swipe top card or whole bundle
),
)
- Single-Card Swipe: Swiping the top card in a bundle dismisses only that notification and automatically surfaces the next hidden member.
- Group Indicator Bar: Displays non-overlapping bundle indicators with dynamic padding and theme adaptation.
3. Priority Triage & Backpressure Eviction
FNQ includes a semantic priority triage engine (NotificationPriority.low, normal, high, critical):
const AppNotification(
title: 'Database Outage',
message: 'Primary DB node unresponsive!',
channelName: 'error',
priority: NotificationPriority.critical,
);
- Priority Auto-Sorting: Pending notifications auto-sort by priority so critical items display first.
- Preemption Eviction: If a queue is full (
maxStackSize) and a higher-priority notification arrives, FNQ automatically evicts the lowest-priority active card (emittingDismissReason.evicted), returns it to the pending queue, and displays the critical notification immediately. - Overflow Strategy: Configure
QueueOverflowStrategy.discardOldestordiscardNewestwhen pending queue capacity (maxPendingSize) is reached.
4. Dynamic Channel Parking
Enable channels to dynamically learn new queue positions when users drag notifications across the screen:
final controller = NotificationController(
enableDynamicChannelParking: true,
);
When enabled, relocating a card from topRight to bottomRight dynamically re-routes future notifications dispatched on that channel to bottomRight.
5. Observability & Event History Log
Observe lifecycle events in real-time or query an in-memory LIFO history ring buffer:
// 1. Subscribe to live stream
controller.events.listen((event) {
switch (event) {
case NotificationQueued(:final notification):
print('Displayed: ${notification.id}');
case NotificationDismissed(:final notification, :final reason):
print('Dismissed: ${notification.id} ($reason)');
case NotificationRelocated(:final notification, :final from, :final to):
print('Relocated from ${from.name} to ${to.name}');
default:
break;
}
});
// 2. Query bounded history log (Opt-in via maxHistoryEntries)
final history = controller.getHistory(
channelName: 'error',
limit: 10,
);
Note
Set maxHistoryEntries: 0 (the default) for zero runtime memory or CPU overhead when history logging is not needed.
6. In-Place Runtime Reconfiguration
Update queues, channels, dynamic parking, and history settings live without re-instantiating the controller or clearing active visible cards:
controller.reconfigure(
queues: {
const NotificationQueue(
position: QueuePosition.topRight,
maxStackSize: 5,
),
},
enableDynamicChannelParking: true,
);
7. Custom Transitions & Styling
Choose from standard queue styles or provide custom transition animations and widget builders:
// Outlined Queue Style
const OutlinedQueueStyle(
borderRadius: BorderRadius.all(Radius.circular(10)),
borderWidth: 1.5,
elevation: 4,
);
// Scale Transition
const ScaleTransitionStrategy(
initialScale: 0.7,
curve: Curves.backOut,
);
// Custom Animation Builder
NotificationQueue(
position: QueuePosition.topCenter,
transition: BuilderTransitionStrategy(
(context, animation, position, child) => FadeTransition(
opacity: animation,
child: SlideTransition(
position: Tween<Offset>(
begin: const Offset(0, -0.5),
end: Offset.zero,
).animate(animation),
child: child,
),
),
),
);
🌐 Platform & Accessibility Support
- RTL Detection: Automatic text direction detection for Arabic, Persian, Hebrew, and Urdu.
- Safe Area Integration: Automatic handling of notches, status bars, and navigation bars.
- Desktop & Web: Mouse hover detection, desktop scroll wheel support, and progressive close button visibility (
VisibleOnHover,AlwaysVisible,Hidden). - Haptics & Touch: Native touch gesture feedback and spring snapback physics.
📈 Migration Guide (v0.x to v1.0)
| Legacy v0.x API | Modern v1.0 Architecture | Description |
|---|---|---|
FlutterNotificationQueue.configure(...) |
final controller = NotificationController(...) |
Pure Dart configuration owner |
MaterialApp(builder: FlutterNotificationQueue.builder) |
NotificationScope(controller: controller, child: child!) |
Scoped widget tree overlay binding |
NotificationWidget(message: '...').show() |
controller.show(AppNotification(message: '...')) |
Pure data payload intent |
NotificationScope.of(context).show(...) |
NotificationScope.of(context).show(AppNotification(...)) |
Context-aware dispatching |
FlutterNotificationQueue.events |
controller.events |
Stable broadcast stream |
For full step-by-step details, see doc/migration_v0_4.md.
📄 License
This project is licensed under the BSD 3-Clause License - see the LICENSE file for details.