flutter_notification_flow 0.1.0 copy "flutter_notification_flow: ^0.1.0" to clipboard
flutter_notification_flow: ^0.1.0 copied to clipboard

A provider-independent Flutter package for normalizing and reliably routing notification interactions across different notification sources and application states.

flutter_notification_flow #

A provider-independent Flutter package for normalizing and reliably routing notification interactions across different notification sources and application states.

CI pub package license style: flutter lints


πŸ’‘ Overview #

Flutter applications frequently receive notification interactions from diverse sources:

  • Firebase Cloud Messaging (FCM)
  • Flutter Local Notifications
  • OneSignal / Pusher / WebSockets / Custom Backends

These notification taps occur across distinct application lifecycles:

  • Foreground events
  • Background events (user taps notification tray)
  • Terminated state / Cold start launches

Without a unified routing layer, developers end up duplicating navigation logic across onMessageOpenedApp, getInitialMessage, local notification callbacks, and deep links. This causes race conditions during startup, duplicate route pushes, and difficult-to-maintain spaghetti code.

flutter_notification_flow provides a clean, provider-independent layer that normalizes raw notification data into an immutable NotificationPayload and routes it to registered handlers or event callbacksβ€”complete with built-in cold-start queueing, sliding-window deduplication, and observable lifecycle streams.


✨ Features #

  • 🎯 Provider-Independent: Zero required dependencies on Firebase, OneSignal, or any push service. Connect any provider in just a few lines.
  • πŸ“¦ Normalized Payloads: Converts flat maps, nested maps, and JSON strings into an immutable NotificationPayload.
  • 🧭 Dual Routing Models:
    • Route-Based Handlers: Execute context-aware handlers using Flutter's NavigatorState.
    • Event-Based Callbacks: Listen via onNotification for frameworks like GetX, GoRouter, Riverpod, and Bloc.
  • ⏳ Cold Start & Lifecycle Readiness: Buffers notification events in a FIFO memory queue if the app is still launching or the Navigator is not ready, then automatically processes them when mounted.
  • πŸ›‘οΈ Duplicate Protection: Configurable sliding time-window deduplication using notification IDs or deterministic payload fingerprints.
  • ⚠️ Graceful Fallbacks & Error Isolation: Handles unknown notification types and runtime handler exceptions without crashing the host app.
  • πŸ“‘ Observable Event Stream: Broadcast stream of lifecycle events (received, queued, handling, handled, duplicate, unhandled, failed).
  • πŸͺΆ Lightweight & Fast: Pure Dart and Flutter SDK capabilities only with zero unnecessary dependencies.

πŸ›οΈ How It Works #

Notification Sources (FCM, Local Notifications, OneSignal, etc.)
                          β”‚
                          β–Ό
              Normalize Payload Data
     (NotificationPayload.fromMap / fromJson)
                          β”‚
                          β–Ό
                  NotificationFlow
                          β”‚
       β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
       β–Ό                                     β–Ό
Context Available?                     Context Unavailable? (Cold start)
       β”‚                                     β”‚
Route Resolver & Deduplication         Enqueue into NotificationQueue (FIFO)
       β”‚                                     β”‚
       β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
       β–Ό                                     β–Ό                            β–Ό
Route Handler                         Event Callback              Unhandled Callback
(Navigator push / dialog)         (GetX / GoRouter / Riverpod)   (Logging / Analytics)
       β”‚                                     β”‚                            β”‚
       β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                             β”‚
                                             β–Ό
                              Application Action / Navigation

πŸ“¦ Installation #

Add flutter_notification_flow to your pubspec.yaml:

dependencies:
  flutter_notification_flow: ^0.1.0

Then install dependencies:

flutter pub get

πŸš€ Quick Start #

import 'package:flutter/material.dart';
import 'package:flutter_notification_flow/flutter_notification_flow.dart';

final navigatorKey = GlobalKey<NavigatorState>();

final notificationFlow = NotificationFlow(
  navigatorKey: navigatorKey,
  routes: {
    'chat': (context, payload) {
      final chatId = payload.data['chatId'];
      Navigator.of(context).pushNamed('/chat', arguments: chatId);
    },
    'post': (context, payload) {
      final postId = payload.data['postId'];
      Navigator.of(context).pushNamed('/post', arguments: postId);
    },
  },
  onUnhandledNotification: (payload) {
    debugPrint('Unhandled notification type: ${payload.type}');
  },
);

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await notificationFlow.initialize();
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      navigatorKey: navigatorKey,
      home: const Scaffold(
        body: Center(child: Text('Home Screen')),
      ),
    );
  }
}

πŸ“– Basic Usage #

When using standard Flutter navigation, provide a GlobalKey<NavigatorState> and register route handlers:

final notificationFlow = NotificationFlow(
  navigatorKey: navigatorKey,
  routes: {
    'chat': (context, payload) async {
      final chatId = payload.data['chatId'];
      await Navigator.of(context).push(
        MaterialPageRoute(builder: (_) => ChatPage(chatId: chatId)),
      );
    },
    'profile': (context, payload) {
      final userId = payload.data['userId'];
      Navigator.of(context).push(
        MaterialPageRoute(builder: (_) => ProfilePage(userId: userId)),
      );
    },
  },
);

Option B: Event-Based Handling (Custom Navigation / State Management) #

If your app uses GetX, GoRouter, Riverpod, or Bloc, you can handle notification interactions without requiring a navigatorKey:

final notificationFlow = NotificationFlow(
  onNotification: (payload) {
    switch (payload.type) {
      case 'chat':
        // Example with GoRouter:
        // context.go('/chat/${payload.data['chatId']}');
        break;
      case 'profile':
        // Example with GetX:
        // Get.toNamed('/profile', arguments: payload.data);
        break;
    }
  },
);

πŸ—‚οΈ Notification Payload Structure #

NotificationPayload automatically normalizes both nested and flat structures:

Nested Payload:

{
  "type": "chat",
  "id": "notif_101",
  "data": {
    "chatId": "c_99",
    "sender": "Alice"
  }
}

Flat Payload:

{
  "type": "chat",
  "id": "notif_101",
  "chatId": "c_99",
  "sender": "Alice"
}

Both produce identical normalized NotificationPayload instances:

  • payload.type βž” 'chat'
  • payload.id βž” 'notif_101'
  • payload.data βž” {'chatId': 'c_99', 'sender': 'Alice'}

Common alternative keys such as notification_type, notificationType, notification_id, message_id, and messageId are also parsed seamlessly.


πŸ”Œ Provider Integrations #

Firebase Cloud Messaging (FCM) #

The package remains completely provider-independent. Integrate Firebase Messaging in your application code:

import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter_notification_flow/flutter_notification_flow.dart';

void setupFirebaseMessaging(NotificationFlow flow) async {
  // 1. App opened from terminated state (cold start)
  final initialMessage = await FirebaseMessaging.instance.getInitialMessage();
  if (initialMessage != null) {
    await flow.handleMap(initialMessage.data);
  }

  // 2. App opened from background
  FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
    flow.handleMap(message.data);
  });
}

Flutter Local Notifications #

Forward tapped local notification payload strings directly:

import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:flutter_notification_flow/flutter_notification_flow.dart';

void onSelectNotification(NotificationResponse response, NotificationFlow flow) {
  final payloadString = response.payload;
  if (payloadString != null && payloadString.isNotEmpty) {
    flow.handleJson(payloadString);
  }
}

πŸ›‘οΈ Duplicate Notification Protection #

Notifications can occasionally be processed multiple times during rapid app launches (for instance, when getInitialMessage and onMessageOpenedApp both fire for the same interaction).

NotificationDuplicateGuard caches recently seen notifications in memory:

final notificationFlow = NotificationFlow(
  config: const NotificationFlowConfig(
    enableDuplicateProtection: true,
    duplicateCacheDuration: Duration(minutes: 5), // Cache sliding window
    enableDebugLogs: true,
  ),
);

Deduplication Strategy:

  1. Explicit Notification ID: If payload.id is present, it is used as the unique key.
  2. Deterministic Data Fingerprint: If payload.id is omitted, a deterministic fingerprint is generated based on payload.type and recursively sorted key-value pairs of payload.data.

⏳ Cold Start & Pending Queue #

When a notification arrives before the Flutter widget tree or NavigatorState is mounted (navigatorKey.currentContext == null):

  1. The notification is placed into an in-memory FIFO NotificationQueue.
  2. A NotificationFlowStatus.queued event is emitted on the event stream.
  3. When notificationFlow.initialize() or processPendingNotifications() is called, pending notifications are drained and executed in strict arrival order.

πŸ“‘ Event Stream & Observability #

Subscribe to events to monitor lifecycle status transitions across your entire app:

final subscription = notificationFlow.events.listen((event) {
  print('Status: ${event.status}, Type: ${event.payload.type}');
});

Lifecycle Statuses (NotificationFlowStatus) #

Status Description
received The notification was received by NotificationFlow.
queued Navigator context is not yet ready; payload was queued.
handling Handler execution has started.
handled Handler or callback completed successfully.
unhandled No handler was registered for this notification type.
duplicate The notification was identified as a duplicate and ignored.
failed An error occurred during routing or handler execution.

⚠️ Error Handling #

Errors during route execution or payload parsing are isolated and reported through onError without crashing the application:

final notificationFlow = NotificationFlow(
  onError: (payload, error, stackTrace) {
    debugPrint('NotificationFlow error handling "${payload?.type}": $error');
  },
);

πŸ“š API Reference #

NotificationFlow #

  • NotificationFlow({navigatorKey, routes, onNotification, onUnhandledNotification, onError, config}) β€” Main coordinator.
  • Future<void> initialize() β€” Initializes flow and flushes queued cold-start notifications.
  • Future<bool> handle(NotificationPayload payload, {BuildContext? context}) β€” Handles normalized payload.
  • Future<bool> handleMap(Map<dynamic, dynamic> map, {BuildContext? context}) β€” Parses map and handles payload.
  • Future<bool> handleJson(String jsonString, {BuildContext? context}) β€” Parses JSON string and handles payload.
  • Future<void> processPendingNotifications({BuildContext? context}) β€” Manually processes queued notifications.
  • void registerRoute(String type, NotificationRouteHandler handler) β€” Dynamically registers a route handler.
  • void registerRoutes(Map<String, NotificationRouteHandler> routes) β€” Registers multiple route handlers.
  • bool unregisterRoute(String type) β€” Removes a registered route handler.
  • bool hasRoute(String type) β€” Checks if a route handler exists.
  • Stream<NotificationFlowEvent> get events β€” Broadcast stream of lifecycle events.
  • int get pendingCount β€” Number of pending notifications currently queued.
  • List<NotificationPayload> get pendingNotifications β€” Snapshot of queued notifications.
  • Future<void> dispose() β€” Releases stream controllers and internal caches.

NotificationPayload #

  • NotificationPayload({required type, required data, id}) β€” Immutable payload.
  • NotificationPayload.fromMap(Map<dynamic, dynamic> map) β€” Factory from raw map.
  • NotificationPayload.fromJson(String jsonString) β€” Factory from JSON string.
  • String get fingerprint β€” Deterministic unique string for deduplication.
  • Map<String, dynamic> toMap() / String toJson() β€” Serialization helpers.

NotificationFlowConfig #

  • enableDuplicateProtection: bool (default: true)
  • duplicateCacheDuration: Duration (default: Duration(minutes: 5))
  • enableDebugLogs: bool (default: false)
  • autoProcessQueueOnReady: bool (default: true)

πŸ“± Example Application #

A complete example application is available in the example/ directory.

To run the example app locally:

cd example
flutter pub get
flutter run

🀝 Contributing #

Contributions are welcome! Please check out CONTRIBUTING.md for guidelines on code formatting, static analysis, running tests, and opening pull requests.


πŸ“„ License #

Distributed under the MIT License. See LICENSE for details.

0
likes
160
points
141
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

A provider-independent Flutter package for normalizing and reliably routing notification interactions across different notification sources and application states.

Repository (GitHub)
View/report issues
Contributing

Topics

#flutter #notifications #navigation #routing #push-notifications

License

MIT (license)

Dependencies

flutter

More

Packages that depend on flutter_notification_flow