Firebase Messaging Handler Plugin
Firebase Cloud Messaging for Flutter apps that need more than token retrieval: click streams, local display, scheduling, quiet hours, inbox storage, diagnostics, and in-app messages.
Table of Contents
- Quick Start
- Key Features
- What You Get
- Documentation Index
- Installation
- Setup
- Usage Examples
- Advanced Features
- In-App Messaging
- Foreground Notification Customization
- Analytics Integration
- Notification Diagnostics
- Quiet Hours & Throttling
- Data-Only Bridging
- Testing Utilities
- Payload Cookbook
- API Reference
- Configuration
- Troubleshooting
- Contributing
- License
- Support
- What's Next?
Quick Start
import 'package:firebase_messaging_handler/firebase_messaging_handler.dart';
// Initialize the plugin
final Stream<NotificationData?>? clickStream = await FirebaseMessagingHandler.instance.init(
senderId: 'your_sender_id',
androidChannelList: [
NotificationChannelData(
id: 'default_channel',
name: 'Default Notifications',
description: 'Default notification channel',
importance: NotificationImportanceEnum.high,
priority: NotificationPriorityEnum.high,
playSound: true,
enableVibration: true,
enableLights: true,
),
],
androidNotificationIconPath: '@drawable/ic_notification',
updateTokenCallback: (fcmToken) async {
// Send token to your backend
print('FCM Token: $fcmToken');
return true;
},
);
// Listen to notification clicks
clickStream?.listen((NotificationData? data) {
if (data != null) {
print('Notification clicked: ${data.title}');
// Handle notification click
}
});
Key Features
Core Features
- Cross-platform support - Android, iOS, Web, plus desktop local-mode support on Windows/Linux
- Unified notification stream - Handle foreground, background, and terminated clicks in one place
- Initial notification control - Choose stream delivery or separate startup handling
- Token management - Fetch, cache, clear, and sync FCM tokens with a backend callback
- Failure visibility - Surface token, permission, badge, and background-handler issues through logs and diagnostics
Advanced Features
- Interactive notification actions - Custom buttons with payload handling
- Notification scheduling - One-time and recurring notifications with device-timezone-aware scheduling
- Badge management - Cross-platform badge count helpers
- Notification grouping - Android groups and iOS conversation threads
- Custom sound support - Platform-specific sound customization
- Analytics hooks - Track notification events with your own analytics callback
- Testing utilities - Mock data and streams for unit and widget tests
- Notification doctor - Diagnose permissions, tokens, badges, and background wiring in seconds
- Web-safe fallbacks - Degrade scheduling/actions/badges when unsupported in browsers
- Quiet hours & frequency caps - Control delivery cadence with lifecycle-aware helpers
- Data-only bridging - Promote silent payloads into local notifications when needed
- Inbox storage - Typed inbox model with SharedPreferences default and in-memory test store for read/delete flows
- Unified handler - Single callback for foreground/background/terminated with normalized payloads
- In-app messaging - Trigger in-app templates from silent FCM payloads
- Foreground controls - Customize fallback foreground notifications
- In-app templates - Welcome, promotion, alert, success, and info templates
- Activity timeline - Persistent notification history with detailed timestamps
- Setup retry logic - Retry Firebase setup when the error is recoverable
- Guided setup - Firebase configuration checks with package name guidance
What You Get
Start with the click stream, then opt into the pieces your app actually needs.
- Core (always on): unified click stream, terminated-notification getter, token lifecycle management, platform badge helpers.
- Optional power-ups: scheduling, recurring rules, grouping, custom sounds, analytics callbacks, in-app templates, foreground overrides, mock/testing utilities.
- One package dependency: the plugin depends on
firebase_messagingfor you, so your app adds this package and configures Firebase once. - Progressive adoption: wire up the click stream today, add interactive actions or in-app templates later without touching existing code.
- Configuration-at-callsite: all advanced APIs expose per-call parameters so you can tailor a single notification without changing global settings.
- Navigation flexibility: Showcase example routes via a root
Navigatorkey, demonstrating payload-driven navigation without relying on a BuildContext. - Desktop local mode: on Windows/Linux, FCM APIs are disabled gracefully while local notifications, scheduling, inbox, quiet hours, and in-app templates remain available.
Documentation Index
Use the README as the landing page, then jump to the deeper guides:
- Best starting point: begin with Installation, then follow the platform setup guide for the targets you ship.
- Getting started: Installation, Android setup, iOS setup, macOS setup, Desktop setup, Web setup
- Feature guides: Push notifications, In-app messaging, Notification inbox, Scheduling, Badges, Quiet hours, Diagnostics, Server recipes
- Release process: Release checklist
- Backend payloads: Payload cookbook, Server recipes
- Project docs: Contributing, Security, Example app guide
Why this over raw Firebase or Awesome Notifications?
| Capability | Raw firebase_messaging | Awesome Notifications | This package |
|---|---|---|---|
| Background & terminated clicks | Manual isolate wiring | Yes | Automatic unified stream |
| Badges & sounds | Manual per-platform | Yes, heavier setup | Built in |
| In-app UI / inbox | None | Partial, paid features | Inbox widget + in-app templates |
| License | BSD | Commercial/freemium | BSD-3-Clause |
| Setup effort | High | Medium/heavy | Lower, with diagnostics |
Current focus: keep setup diagnostics, platform guidance, and release validation explicit so teams can ship FCM without rediscovering platform-specific edge cases.
Architecture Benefits
- Modular design - Clear separation between facade, managers, services, interfaces, and models
- Testability - Interface-based services and in-memory stores keep tests practical
- Maintainability - Features are grouped by lifecycle, presentation, storage, and diagnostics concerns
- Backward compatibility - Existing public APIs are kept stable across patch releases
- Reasonable runtime cost - Work is mostly event-driven and initialized lazily where possible
Installation
Add this to your package's pubspec.yaml file:
dependencies:
firebase_messaging_handler: ^1.0.6
Setup
Quick Setup
-
Add dependency:
dependencies: firebase_messaging_handler: ^1.0.6 -
Add basic permissions to
android/app/src/main/AndroidManifest.xml:<!-- Basic Firebase Messaging --> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.VIBRATE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />Need scheduling, actions, or boot restore? See the Detailed Permissions Guide below.
-
Initialize in your app:
await FirebaseMessagingHandler.instance.init( senderId: 'your_sender_id', androidChannelList: [/* channels */], androidNotificationIconPath: '@drawable/ic_notification', ); -
(Optional) Wire the background handler:
await FirebaseMessagingHandler.instance.configureBackgroundMessageHandler( firebaseMessagingHandlerBackgroundDispatcher, );Use your own top-level handler if you need custom logic—just remember to call
FirebaseMessagingHandler.handleBackgroundMessage(message)first.
Unified Handler (all lifecycles)
await FirebaseMessagingHandler.instance.setUnifiedMessageHandler(
(NormalizedMessage message, NotificationLifecycle lifecycle) async {
debugPrint('[unified] lifecycle=$lifecycle title=${message.title}');
// Return true to mark handled and skip default rendering; false to let the plugin render/queue.
if (lifecycle == NotificationLifecycle.foreground) {
// e.g., custom in-app banner instead of system notification
return true;
}
return false;
},
);
Handler receives normalized fields (id, title, body, data, channelId, analytics, lifecycle, rawMessage). Works for foreground, background, resume, and terminated paths.
- Done. Your app now has Firebase notification handling wired.
Minimal Setup (Basic Notifications Only)
For apps that only need basic push notifications:
<!-- Minimal permissions for basic notifications -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.VIBRATE" />
What you get:
- Push notifications from Firebase
- Background message handling
- Notification vibration
- No scheduled notifications
- No foreground notifications
- No advanced features
Detailed Setup
1. Firebase Project Setup
- Create a Firebase project at Firebase Console
- Add your Android and iOS apps to the project
- Download configuration files:
google-services.json→android/app/GoogleService-Info.plist→ios/Runner/
2. Platform Configuration
Android Setup
Add to android/app/build.gradle:
dependencies {
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")
}
Android Permissions Guide
Choose only the permissions you need based on your features:
Basic Notifications (Most Apps Need This)
<!-- REQUIRED: Basic Firebase Messaging -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<!-- REQUIRED: Notification Display -->
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
When to use: Basic push notifications, message handling, foreground notifications
Scheduled Notifications (Optional)
<!-- REQUIRED: Scheduled Notifications -->
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission android:name="android.permission.USE_EXACT_ALARM" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
When to use: Only if you use scheduleNotification() or scheduleRecurringNotification()
Advanced Features (Optional)
<!-- REQUIRED: Background Processing -->
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<!-- REQUIRED: Notification Actions -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
When to use: Interactive notifications, background processing, notification actions
Quick Decision Guide:
| Feature | Permissions Needed |
|---|---|
| Basic push notifications | INTERNET + WAKE_LOCK + VIBRATE + FOREGROUND_SERVICE |
| Scheduled notifications | Add SCHEDULE_EXACT_ALARM + USE_EXACT_ALARM + RECEIVE_BOOT_COMPLETED |
| Interactive notifications | Add FOREGROUND_SERVICE (already included in basic) |
| Background processing | Add RECEIVE_BOOT_COMPLETED |
Start with the basic permissions, then add the optional permissions only when you use the matching feature.
Why These Permissions?
| Permission | Why We Need It | What Happens Without It |
|---|---|---|
INTERNET |
Firebase messaging requires internet connection | No push notifications |
WAKE_LOCK |
Keeps device awake to process background messages | Messages may be missed while the device sleeps |
VIBRATE |
Makes notifications noticeable | Notifications are silent |
FOREGROUND_SERVICE |
Shows notifications when app is active | Foreground notifications may not display |
SCHEDULE_EXACT_ALARM |
Allows precise notification timing | Exact scheduled notifications fail |
USE_EXACT_ALARM |
Required for exact alarm scheduling | Exact scheduled notifications fail |
RECEIVE_BOOT_COMPLETED |
Restores scheduled notifications after reboot | Scheduled notifications are lost after reboot |
iOS Setup
Add to ios/Runner/AppDelegate.swift:
import Firebase
@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
FirebaseApp.configure()
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
iOS APNs Setup Required:
For iOS notifications to work properly, you MUST configure APNs:
-
Generate APNs Key:
- Go to Apple Developer Console
- Navigate to Certificates, Identifiers & Profiles
- Go to Keys section
- Create a new key with "Apple Push Notifications service (APNs)" enabled
- Download the
.p8key file
-
Upload to Firebase:
- Go to Firebase Console > Project Settings > Cloud Messaging
- Scroll to "Apple app configuration"
- Upload your APNs key (
.p8file) - Enter your Key ID and Team ID
- Choose environment: Sandbox (development) or Production
-
Without APNs setup:
- iOS notifications will NOT work
- FCM tokens will show "APNs token not set" error
- This is normal behavior until APNs is configured
This is a Firebase/APNs requirement, not a plugin limitation.
Web Setup (Optional)
Add to web/index.html:
<script src="https://www.gstatic.com/firebasejs/9.0.0/firebase-app.js"></script>
<script src="https://www.gstatic.com/firebasejs/9.0.0/firebase-messaging.js"></script>
Browser caveats: Browsers do not support local scheduling, notification action buttons, or app-icon badges. Calls to those APIs are safely ignored and surfaced by the diagnostics helper.
Usage Examples
Working Example
import 'package:flutter/material.dart';
import 'package:firebase_messaging_handler/firebase_messaging_handler.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Initialize Firebase
await Firebase.initializeApp();
// Initialize Firebase Messaging Handler
final Stream<NotificationData?>? clickStream = await FirebaseMessagingHandler.instance.init(
senderId: 'your_sender_id',
androidChannelList: [
NotificationChannelData(
id: 'default_channel',
name: 'Default Notifications',
description: 'Default notification channel',
importance: NotificationImportanceEnum.high,
priority: NotificationPriorityEnum.high,
playSound: true,
enableVibration: true,
),
],
androidNotificationIconPath: '@drawable/ic_notification',
updateTokenCallback: (fcmToken) async {
print('FCM Token: $fcmToken');
// Send token to your backend
return true;
},
);
// Listen to notification clicks
clickStream?.listen((NotificationData? data) {
if (data != null) {
print('Notification clicked: ${data.title}');
// Handle notification click
}
});
// Initial launch notifications are emitted onto the same stream by default.
// Set includeInitialNotificationInStream: false to opt out if you need to
// defer handling (e.g., until after auth).
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Firebase Messaging Handler Demo',
home: MyHomePage(),
);
}
}
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('FCM Handler Demo')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () async {
// Show a test notification
await FirebaseMessagingHandler.instance.showNotificationWithActions(
title: 'Test Notification',
body: 'This is a test notification',
actions: [
NotificationAction(
id: 'reply',
title: 'Reply',
payload: {'action': 'reply'},
),
NotificationAction(
id: 'dismiss',
title: 'Dismiss',
payload: {'action': 'dismiss'},
),
],
);
},
child: Text('Send Test Notification'),
),
SizedBox(height: 16),
ElevatedButton(
onPressed: () async {
// Schedule a notification
await FirebaseMessagingHandler.instance.scheduleNotification(
id: 1,
title: 'Scheduled Notification',
body: 'This notification was scheduled',
scheduledDate: DateTime.now().add(Duration(minutes: 1)),
);
},
child: Text('Schedule Notification'),
),
],
),
),
);
}
}
Showcase Example App
The example/ directory doubles as an FCM showcase powered by this plugin:
- Guided onboarding banner - copy your FCM token, open the Firebase console, and follow the three-step testing loop.
- Quick start scenarios - fire interactive pushes, schedule one-time or recurring notifications, and generate Android groups with a tap.
- Power utilities - update badges, register custom sound channels, and clear demo data while analytics events stream in.
- Scenario detail screen - every notification routes to a dedicated inspector showing payloads, actions, badges, and metadata.
- Activity timeline - watch a running log of initialization, scheduling, clears, and custom actions.
- Template playground - paste sample silent payloads to preview the generic template renderer in real time.
Run flutter run inside example/ to explore it.
Basic Setup
// Initialize the plugin
final Stream<NotificationData?>? clickStream = await FirebaseMessagingHandler.instance.init(
senderId: 'your_sender_id',
androidChannelList: channels,
androidNotificationIconPath: '@drawable/ic_notification',
updateTokenCallback: (fcmToken) async {
// Send token to your backend
return true;
},
);
// Listen to notification clicks
clickStream?.listen((NotificationData? data) {
if (data != null) {
// Handle notification click
}
});
Interactive Notifications
// Show notification with action buttons
await FirebaseMessagingHandler.instance.showNotificationWithActions(
title: 'New Message',
body: 'You have a new message from John',
actions: [
NotificationAction(
id: 'reply',
title: 'Reply',
payload: {'action': 'reply', 'user_id': '123'},
),
NotificationAction(
id: 'view',
title: 'View',
payload: {'action': 'view', 'message_id': '456'},
),
NotificationAction(
id: 'dismiss',
title: 'Dismiss',
payload: {'action': 'dismiss'},
),
],
);
Inbox Storage (typed, persistent)
Use the default SharedPreferences-backed inbox store to fuel a history or inbox UI with read/delete support.
final inbox = InboxStorageService();
await inbox.upsert(
NotificationInboxItem(
id: 'welcome',
title: 'Welcome!',
body: 'Thanks for installing the app.',
timestamp: DateTime.now(),
data: {'origin': 'campaign_welcome'},
),
);
final List<NotificationInboxItem> page =
await inbox.fetch(page: 0, pageSize: 20);
await inbox.markRead([page.first.id]);
await inbox.delete([page.first.id]);
For tests or ephemeral state, use
InMemoryInboxStorage, which keeps items purely in memory.
Inbox UI widget
NotificationInboxView(
storage: InboxStorageService(),
onItemTap: (item) {
// Navigate or open detail
},
onActionTap: (actionId, item) {
// Handle custom buttons stored in item.actions
},
onDelete: (ids) async {
// Optional: sync deletions to backend
},
);
Notification Scheduling
Scheduled notifications use the device timezone. If your app reschedules reminders on resume, refresh the timezone first:
await FirebaseMessagingHandler.instance.refreshLocalTimezone();
// Schedule a one-time notification
await FirebaseMessagingHandler.instance.scheduleNotification(
id: 1,
title: 'Meeting Reminder',
body: 'Team meeting in 30 minutes',
scheduledDate: DateTime.now().add(Duration(minutes: 30)),
);
// Schedule a recurring notification
await FirebaseMessagingHandler.instance.scheduleRecurringNotification(
id: 2,
title: 'Daily Reminder',
body: 'Don\'t forget to check your tasks',
repeatInterval: 'daily',
hour: 9,
minute: 0,
);
Badge Management
// Set badge count
await FirebaseMessagingHandler.instance.setIOSBadgeCount(5);
await FirebaseMessagingHandler.instance.setAndroidBadgeCount(3);
// Get badge count
final int iosBadge = await FirebaseMessagingHandler.instance.getIOSBadgeCount();
final int androidBadge = await FirebaseMessagingHandler.instance.getAndroidBadgeCount();
// Clear badge count
await FirebaseMessagingHandler.instance.clearBadgeCount();
Notification Grouping
// Show grouped notifications
await FirebaseMessagingHandler.instance.showGroupedNotification(
title: 'New Messages',
body: 'You have 3 new messages',
groupKey: 'messages',
groupTitle: 'Messages',
isSummary: true,
);
// Create notification group
await FirebaseMessagingHandler.instance.createNotificationGroup(
groupKey: 'messages',
groupTitle: 'Messages',
notifications: [
NotificationData(
title: 'Message 1',
body: 'Hello from John',
payload: {'message_id': '1'},
),
NotificationData(
title: 'Message 2',
body: 'Hello from Jane',
payload: {'message_id': '2'},
),
],
);
Custom Sounds
// Create custom sound channel
await FirebaseMessagingHandler.instance.createCustomSoundChannel(
channelId: 'custom_sound',
channelName: 'Custom Sound Notifications',
channelDescription: 'Notifications with custom sounds',
soundFileName: 'custom_sound.mp3',
importance: NotificationImportanceEnum.high,
priority: NotificationPriorityEnum.high,
);
// Show notification with custom sound
await FirebaseMessagingHandler.instance.showNotificationWithCustomSound(
title: 'Custom Sound Notification',
body: 'This notification has a custom sound',
soundFileName: 'custom_sound.mp3',
);
Analytics Integration
// Set up analytics callback
FirebaseMessagingHandler.instance.setAnalyticsCallback((event, data) {
print('Analytics Event: $event');
print('Event Data: $data');
// Send to your analytics service
// FirebaseAnalytics.instance.logEvent(name: event, parameters: data);
});
// Track custom events
FirebaseMessagingHandler.instance.trackAnalyticsEvent('custom_event', {
'user_id': '123',
'action': 'notification_clicked',
});
Advanced Features
Notification Actions
Create interactive notifications with custom action buttons:
NotificationAction(
id: 'reply',
title: 'Reply',
destructive: false,
payload: {
'action': 'reply',
'user_id': '123',
'thread_id': '456',
},
)
Scheduling Options
Schedule notifications with various options:
// One-time notification
await FirebaseMessagingHandler.instance.scheduleNotification(
id: 1,
title: 'One-time Notification',
body: 'This will show once',
scheduledDate: DateTime.now().add(Duration(hours: 1)),
allowWhileIdle: true,
);
// Recurring notification
await FirebaseMessagingHandler.instance.scheduleRecurringNotification(
id: 2,
title: 'Daily Reminder',
body: 'Daily task reminder',
repeatInterval: 'daily',
hour: 9,
minute: 0,
);
Badge Management
Cross-platform badge count management:
// iOS badge management
await FirebaseMessagingHandler.instance.setIOSBadgeCount(5);
final int iosBadge = await FirebaseMessagingHandler.instance.getIOSBadgeCount();
// Android badge management
await FirebaseMessagingHandler.instance.setAndroidBadgeCount(3);
final int androidBadge = await FirebaseMessagingHandler.instance.getAndroidBadgeCount();
// Clear all badges
await FirebaseMessagingHandler.instance.clearBadgeCount();
Notification Grouping
Organize notifications into groups:
// Android notification groups
await FirebaseMessagingHandler.instance.showGroupedNotification(
title: 'Group Summary',
body: '3 new notifications',
groupKey: 'messages',
groupTitle: 'Messages',
isSummary: true,
);
// iOS conversation threads
await FirebaseMessagingHandler.instance.showThreadedNotification(
title: 'New Message',
body: 'Hello from John',
threadIdentifier: 'conversation_123',
);
Custom Sound Support
Platform-specific sound customization:
// Create custom sound channel
await FirebaseMessagingHandler.instance.createCustomSoundChannel(
channelId: 'custom_sound',
channelName: 'Custom Sound Notifications',
channelDescription: 'Notifications with custom sounds',
soundFileName: 'custom_sound.mp3',
importance: NotificationImportanceEnum.high,
priority: NotificationPriorityEnum.high,
enableVibration: true,
enableLights: true,
);
// Get available sounds (iOS)
final List<String>? sounds = await FirebaseMessagingHandler.instance.getAvailableSounds();
In-App Messaging
Deliver rich in-app experiences using silent/data-only FCM payloads that map to reusable templates.
Register Templates
FirebaseMessagingHandler.instance.registerInAppNotificationTemplates({
'promo_banner': InAppNotificationTemplate(
id: 'promo_banner',
description: 'Lightweight promotional banner',
onDisplay: (inAppData) {
inAppOverlayController.showBanner(
title: inAppData.content['title'] as String?,
body: inAppData.content['body'] as String?,
imageUrl: inAppData.content['image'] as String?,
ctaLabel: inAppData.content['cta_label'] as String?,
deeplink: inAppData.content['deeplink'] as String?,
);
},
),
});
FirebaseMessagingHandler.instance.setInAppFallbackDisplayHandler((payload) {
debugPrint('Unhandled in-app template: ${payload.templateId}');
});
Listen for Ready Messages
FirebaseMessagingHandler.instance
.getInAppNotificationStream()
.listen((inAppData) {
inAppLogger.debug('Render template ${inAppData.templateId}');
campaignAnalytics.track('in_app_impression', inAppData.analytics);
});
Need to hydrate pending payloads after a cold start? Call:
await FirebaseMessagingHandler.instance.flushPendingInAppNotifications();
Sample FCM Payload
{
"message": {
"token": "{{deviceToken}}",
"data": {
"fcmh_inapp": "{ \"id\": \"winter_sale_2025\", \"templateId\": \"promo_banner\", \"trigger\": \"immediate\", \"content\": { \"title\": \"Winter Sale\", \"body\": \"Take 25% off today only\", \"cta_label\": \"Shop now\", \"deeplink\": \"app://store/sale\" }, \"analytics\": { \"campaignId\": \"winter_flash\", \"variant\": \"A\" } }"
}
}
}
Supported triggers:
immediate→ render as soon as the payload arrives (foreground or via queued stream)next_foreground→ store until the next time you listen to the streamapp_launch→ store untilflushPendingInAppNotificationsis calledcustom→ surface the payload immediately and let the host decide when to display
Use clearPendingInAppNotifications() to drop queued payloads (optionally targeting a specific id).
Built-in Templates & Overlay Support
Good fits for in-app templates:
- Feature announcements - Introduce new capabilities
- User onboarding - Guide users through app features
- Feedback collection - Gather user ratings and suggestions
- Promotional content - Showcase offers and campaigns
- Educational content - Tips, tutorials, and help
- User engagement - Surveys, polls, and interactive content
- Quick notifications - Snackbars for non-intrusive messages
Avoid in-app templates for critical updates:
- App updates - Use system-level update prompts instead
- Security alerts - Use push notifications for immediate attention
- Payment confirmations - Use dedicated UI flows
- Emergency notifications - Use push notifications for reliability
Template flexibility: The built-in templates are examples. You can:
- Register custom templates with your own layouts and animations
- Create any UI component - modals, sheets, cards, overlays, etc.
- Define custom interactions - gestures, animations, transitions
- Build brand-specific experiences - match your app's design system
- Implement complex workflows - multi-step processes, wizards, etc.
The plugin handles overlay management, navigation hooks, and analytics callbacks while your app owns the actual UI.
Provide a navigator key so the handler can present rich layouts:
final GlobalKey<NavigatorState> rootNavigatorKey = GlobalKey<NavigatorState>();
void main() {
WidgetsFlutterBinding.ensureInitialized();
FirebaseMessagingHandler.instance.setInAppNavigatorKey(rootNavigatorKey);
runApp(MaterialApp(
navigatorKey: rootNavigatorKey,
home: const ShowcaseHome(),
));
}
Register the generic template and handle button callbacks:
void _registerTemplates() {
FirebaseMessagingHandler.instance.registerInAppNotificationTemplates({
'builtin_generic': BuiltInInAppTemplates.generic(
onAction: (actionId, data) {
debugPrint('Template action: $actionId payload=${data.payload}');
},
),
});
}
Trigger locally (useful for testing) or remotely via a silent FCM payload:
InAppMessageManager.instance.triggerInAppNotification(
InAppNotificationData(
id: 'demo_${DateTime.now().millisecondsSinceEpoch}',
templateId: 'builtin_generic',
triggerType: InAppTriggerTypeEnum.immediate,
content: {
'layout': 'dialog',
'title': 'New Feature Available',
'subtitle': 'Enhanced notification controls',
'body': 'We\'ve added smart scheduling and quiet hours. Try them out!',
'imageUrl': 'https://via.placeholder.com/600x320/059669/ffffff?text=New+Feature',
'blurSigma': 16,
'cornerRadius': 20,
'buttons': [
{'id': 'try_now', 'label': 'Try Now', 'style': 'filled'},
{'id': 'learn_more', 'label': 'Learn More', 'style': 'outlined'},
{'id': 'dismiss', 'label': 'Not now', 'style': 'text', 'dismissOnly': true}
],
},
analytics: {'source': 'docs_demo'},
rawPayload: const {},
receivedAt: DateTime.now(),
),
);
Supported layouts include dialog, full_screen, bottom_sheet, banner, tooltip, carousel, and snackbar. Configure blur, barrier color, size factors, button styles, and HTML content directly from the payload.
Key payload fields:
layout: dialog | full_screen | bottom_sheet | banner | snackbarwidthFactor/heightFactor: fractions of the screen size (dialog + full screen)blurSigma&barrierColor: backdrop styling for dialogs/full screensbackgroundColor/textColor: hex (#RRGGBBor#AARRGGBB) or RGB mapshtml: optional HTML body rendered withflutter_widget_from_html_corebuttons: array of{ id, label, style (filled|outlined|text|link), dismissOnly }autoDismissSeconds: auto-dismiss duration for banners/snackbarsposition:toporbottomfor banner layoutpages: list of page maps (carousel) each supportingtitle,body,html,imageUrl, andbuttons
Custom Template Registration
Create your own templates with complete control over UI and behavior:
// Register a custom template
FirebaseMessagingHandler.instance.registerInAppNotificationTemplates({
'my_custom_template': InAppNotificationTemplate(
id: 'my_custom_template',
description: 'Custom onboarding flow',
autoDismissDuration: null, // Manual dismiss
onDisplay: (data) {
// Your custom UI logic here
showDialog(
context: context,
builder: (context) => MyCustomOnboardingDialog(
title: data.content['title'],
steps: data.content['steps'],
onComplete: () => data.onAction?.call('completed', data),
),
);
},
),
'my_animated_banner': InAppNotificationTemplate(
id: 'my_animated_banner',
description: 'Animated promotional banner',
autoDismissDuration: const Duration(seconds: 5),
onDisplay: (data) {
// Custom animated banner with your branding
showAnimatedBanner(
message: data.content['message'],
backgroundColor: data.content['color'],
animation: SlideAnimation.fromTop(),
);
},
),
});
Custom Template Benefits:
- Complete UI control - Use any Flutter widget
- Brand consistency - Match your app's design system
- Advanced animations - Custom transitions and effects
- Complex interactions - Multi-step flows, gestures, etc.
- Platform-specific behavior - Different UIs per platform
- Integration flexibility - Connect to your existing components
Foreground Notification Customization
Own the fallback notification UI that appears while your app is active. The plugin includes smart fallback logic to ensure notifications always display, even when no channel ID is provided.
Smart Channel Fallback
The plugin automatically handles Android notification channels with intelligent fallback:
- Channel Specified: If a notification includes a channel ID, that specific channel is used
- Channel Not Found: If the specified channel doesn't exist, falls back to the first available channel
- No Channel Provided: If no channel ID is specified, uses the first available channel
- No Channels Available: Logs an error and skips the notification (prevents crashes)
This ensures your Android foreground notifications always display, regardless of Firebase Console configuration.
Override Once, Anywhere
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
FirebaseMessagingHandler.instance.setForegroundNotificationOptions(
ForegroundNotificationOptions(
androidBuilder: (context) {
final imageAsset = context.data['image_asset'] as String?;
if (imageAsset != null) {
return AndroidNotificationDetails(
'promo_channel',
'Promotions',
channelDescription: 'Foreground promos',
importance: Importance.max,
priority: Priority.high,
styleInformation: BigPictureStyleInformation(
DrawableResourceAndroidBitmap(imageAsset),
largeIcon: DrawableResourceAndroidBitmap(imageAsset),
),
);
}
return const AndroidNotificationDetails(
'default_channel',
'Default Notifications',
importance: Importance.max,
priority: Priority.high,
);
},
iosBuilder: (context) {
final imageName = context.data['image_asset'] as String?;
if (imageName == null) {
return const DarwinNotificationDetails(
presentAlert: true,
presentSound: true,
presentBadge: true,
);
}
return DarwinNotificationDetails(
presentAlert: true,
presentSound: true,
presentBadge: true,
attachments: [
DarwinNotificationAttachment('resource:///$imageName'),
],
);
},
),
);
The builders receive the real RemoteMessage, so you can map any data payload to advanced styles, media, icons, or badges. Return null to fall back to the plugin defaults, or set enabled: false to suppress the automatic notification entirely when you prefer a custom in-app surface.
Prefer static overrides? Use androidDefaults / iosDefaults to plug in prebuilt AndroidNotificationDetails or DarwinNotificationDetails instances without writing builders.
Configure Default Custom Sounds
Set custom sounds once—they'll apply to all foreground notifications automatically:
// Step 1: Configure default sounds for foreground notifications
FirebaseMessagingHandler.instance.setForegroundNotificationOptions(
ForegroundNotificationOptions(
// Android: Place sound file in android/app/src/main/res/raw/custom_sound.mp3
androidSoundFileName: 'custom_sound', // Without extension
// iOS: Place sound file in project (Runner/Sounds/custom_sound.aiff)
iosSoundFileName: 'custom_sound.aiff', // With extension
androidDefaults: const AndroidNotificationDetails(
'default_channel',
'Default Notifications',
importance: Importance.max,
priority: Priority.high,
),
iosDefaults: const DarwinNotificationDetails(
presentAlert: true,
presentSound: true,
presentBadge: true,
),
),
);
Platform-Specific Sound Configuration:
Android (Two Options):
Option 1: Default sound for all foreground notifications (shown above)
ForegroundNotificationOptions(
androidSoundFileName: 'custom_sound', // Applied to all foreground notifications
)
Option 2: Per-channel sounds (configured during init)
await FirebaseMessagingHandler.instance.init(
senderId: 'your_sender_id',
androidChannelList: [
NotificationChannelData(
id: 'default_channel',
name: 'Default Notifications',
soundFileName: 'default_sound', // Sound for this channel only
),
NotificationChannelData(
id: 'urgent_channel',
name: 'Urgent Alerts',
soundFileName: 'urgent_sound', // Different sound for urgent channel
),
],
androidNotificationIconPath: '@drawable/ic_notification',
);
iOS:
iOS doesn't have channels, so configure the default sound through ForegroundNotificationOptions:
ForegroundNotificationOptions(
iosSoundFileName: 'custom_sound.aiff', // Applied to ALL iOS notifications
)
Important iOS limitation:
Foreground Notifications (App Active):
- Custom sounds work - via
ForegroundNotificationOptions.iosSoundFileName - Plugin-rendered notifications - foreground notifications are handled by the plugin
Background Notifications (App Killed/Backgrounded):
- Plugin defaults are not used - iOS handles these directly
- Payload-driven sound - iOS uses the APNs/FCM payload and bundled sound resources
Background sound setup:
For background notifications, configure the sound in your Firebase Console payload:
{
"notification": {
"title": "Background Notification",
"body": "This will use system default sound",
"sound": "custom_sound.aiff" // iOS will use this if file exists in app bundle
},
"apns": {
"payload": {
"aps": {
"sound": "custom_sound.aiff" // Alternative APNs-specific sound
}
}
}
}
Requirements for Background Sounds:
- Sound file must be in your iOS app bundle (added via Xcode)
- Sound file must be ≤ 30 seconds
- Supported formats: AIFF, CAF, WAV
- If file doesn't exist, iOS falls back to default sound
Sound File Setup:
Android:
- Place sound file in
android/app/src/main/res/raw/ - Use filename without extension (e.g.,
custom_soundforcustom_sound.mp3) - Supported formats: MP3, OGG
iOS:
- Add sound file to Xcode project (via Xcode > Add Files)
- Ensure it's added to the target (check "Copy items if needed")
- Use filename with extension (e.g.,
custom_sound.aiff) - Supported formats: AIFF, CAF, WAV (up to 30 seconds)
Tip:
- Android: Use
NotificationChannelData.soundFileNamefor per-channel sounds, orForegroundNotificationOptions.androidSoundFileNamefor all foreground notifications.- iOS: Use
ForegroundNotificationOptions.iosSoundFileNamefor plugin-rendered foreground notifications. For background notifications, set the sound in the APNs/FCM payload.
Use
DrawableResourceAndroidBitmap,ByteArrayAndroidBitmap, orFilePathAndroidBitmapdepending on where your assets live. For iOS,DarwinNotificationAttachmentexpects a local resource URI, so download remote media before attaching it.
Analytics Integration
Built-in Event Tracking
The plugin automatically tracks these events:
notification_received- When notifications arrivenotification_clicked- When notifications are tappednotification_action- When action buttons are pressednotification_scheduled- When notifications are scheduledfcm_token- Token events (fetched, updated, error)
Custom Analytics
// Set up analytics callback
FirebaseMessagingHandler.instance.setAnalyticsCallback((event, data) {
// Send to your analytics service
FirebaseAnalytics.instance.logEvent(
name: event,
parameters: data,
);
});
// Track custom events
FirebaseMessagingHandler.instance.trackAnalyticsEvent('custom_event', {
'user_id': '123',
'action': 'notification_clicked',
'timestamp': DateTime.now().toIso8601String(),
});
Notification Diagnostics
Stay ahead of production issues with a built-in "notification doctor". It inspects permissions, token state, badge capabilities, web support, and background wiring in one call.
Run the Doctor
final diagnostics = await FirebaseMessagingHandler.instance.runDiagnostics();
debugPrint('Notification diagnostics: ${diagnostics.toMap()}');
if (!diagnostics.success || diagnostics.recommendations.isNotEmpty) {
for (final recommendation in diagnostics.recommendations) {
debugPrint('Recommendation → $recommendation');
}
}
What you get:
permissionsGrantedandauthorizationStatus– current notification permission state.fcmTokenAvailable– whether a token is cached viaupdateTokenCallback.badgeSupported– launcher/platform badge capability (best-effort on Android).webNotificationsAllowed/metadata['webPermission']– browser permission string.metadata['webDiagnostics']– notification API availability, secure-context status, and service-worker/controller checks on web.metadata['fcmSupported']/metadata['fcmUnsupportedReason']– whether Firebase Messaging is available on the current platform, including desktop local-mode fallback on Windows/Linux.metadata['backgroundHandlerRegistered']– confirmsconfigureBackgroundMessageHandlerhas been invoked.pendingNotificationCount– number of locally scheduled notifications.metadata['invalidPayloadCount']– how many malformed data-only payloads were rejected by the bridge/schema guard.
Background Message Helper
Register a top-level handler once and reuse the plugin’s pipeline inside the isolate:
@pragma('vm:entry-point')
Future<void> myBackgroundHandler(RemoteMessage message) async {
WidgetsFlutterBinding.ensureInitialized();
await FirebaseMessagingHandler.handleBackgroundMessage(message);
// Custom logic: update analytics, hydrate local cache, etc.
}
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await FirebaseMessagingHandler.instance.configureBackgroundMessageHandler(
myBackgroundHandler,
);
runApp(const MyApp());
}
Tip: Prefer the built-in
firebaseMessagingHandlerBackgroundDispatcherif you simply want to hydrate the plugin without extra logic:await FirebaseMessagingHandler.instance.configureBackgroundMessageHandler( firebaseMessagingHandlerBackgroundDispatcher, );
Web Safeguards
Scheduling, interactive actions, and app-icon badges are not available in browsers. The doctor highlights these limitations and the runtime API logs “ignored” warnings so you can branch logic per platform.
Quiet Hours & Throttling
Control when in-app messages surface and how frequently campaigns fire.
await FirebaseMessagingHandler.instance.setInAppDeliveryPolicy(
const InAppDeliveryPolicy(
globalInterval: Duration(seconds: 30),
perTemplateInterval: Duration(minutes: 2),
perTemplateDailyCap: 5,
quietHours: InAppQuietHours(startHour: 22, endHour: 7),
),
);
globalIntervalenforces a cool-down between any two in-app presentations.perTemplateIntervalkeeps the same template from spamming the timeline.perTemplateDailyCaplimits impressions per template per day.quietHoursdefers delivery until the configured window closes. Deferred payloads are re-queued automatically with the diagnostics report showing their status.
Data-Only Bridging
Promote silent payloads into local notifications (or custom flows) so users still see timely updates.
// Promote data-only FCM payloads to local notifications automatically
FirebaseMessagingHandler.instance.enableDefaultDataOnlyBridge(
channelId: 'actions_channel',
titleKey: 'title',
bodyKey: 'body',
);
// Or wire your own handler and decide when work is complete
await FirebaseMessagingHandler.instance.configureBackgroundProcessingCallback(
(RemoteMessage message) async {
if (message.data['should_defer'] == 'true') {
return false; // enqueue for retry when app wakes up
}
// Custom processing…
return true;
},
);
Use FirebaseMessagingHandler.handleBackgroundMessage(message) inside your top-level background function to hydrate local queues before running custom logic.
Testing Utilities
Mock Data Generation
// Create mock notification data
final NotificationData mockData = FirebaseMessagingHandler.createMockNotificationData(
title: 'Mock Notification',
body: 'This is a mock notification',
payload: {'test': 'data'},
type: NotificationTypeEnum.foreground,
);
// Create mock remote message
final RemoteMessage mockMessage = FirebaseMessagingHandler.createMockRemoteMessage(
title: 'Mock Message',
body: 'This is a mock message',
data: {'test': 'data'},
);
Test Mode
// Enable test mode
FirebaseMessagingHandler.setTestMode(true);
// Get mock streams
final Stream<RemoteMessage>? mockNotificationStream =
FirebaseMessagingHandler.getMockNotificationStream();
final Stream<NotificationData>? mockClickStream =
FirebaseMessagingHandler.getMockClickStream();
// Add mock events
FirebaseMessagingHandler.addMockNotification(mockMessage);
FirebaseMessagingHandler.addMockClickEvent(mockData);
// Reset mock data
FirebaseMessagingHandler.resetMockData();
Payload Cookbook
Jump-start backend integration with ready-to-send payloads:
For full backend examples, see server_recipes/ with Cloud Functions and FCM HTTP v1 templates.
Interactive Notification (Actions + Analytics)
{
"message": {
"token": "<device-token>",
"notification": {
"title": "New Support Ticket",
"body": "Tap Reply to follow up without opening the app."
},
"data": {
"is_action": true,
"action_id": "reply",
"action_payload": {"ticket_id": "12345"},
"analytics": {"campaign": "support_reengage"}
}
}
}
Data-Only → Local Notification Bridge
{
"message": {
"token": "<device-token>",
"data": {
"title": "Inventory Update",
"body": "SKU #48319 is back in stock!",
"deep_link": "app://inventory/48319"
}
}
}
In-App Template Trigger
{
"message": {
"token": "<device-token>",
"data": {
"fcmh_inapp": {
"id": "promo-2025",
"templateId": "builtin_generic",
"trigger": "immediate",
"content": {
"layout": "html_modal",
"title": "Spring Launch",
"html": "<h2>Fresh features</h2><p>Try quiet hours + notification doctor today.</p>",
"buttons": [{"id": "explore", "label": "Explore", "style": "filled"}]
}
}
}
}
}
API Reference
Core Methods
Initialization
Future<Stream<NotificationData?>?> init({
required String senderId,
required List<NotificationChannelData> androidChannelList,
required String androidNotificationIconPath,
Future<bool> Function(String fcmToken)? updateTokenCallback,
bool includeInitialNotificationInStream = true,
})
Initial Notification Handling
Future<NotificationData?> checkInitial() // optional fallback; auto-handled by default
Notification Display
Future<void> showNotificationWithActions({
required String title,
required String body,
required List<NotificationAction> actions,
Map<String, dynamic>? payload,
String? channelId,
int? notificationId,
})
Future<void> showNotificationWithCustomSound({
required String title,
required String body,
required String soundFileName,
String? channelId,
Map<String, dynamic>? payload,
int? notificationId,
})
Scheduling
Future<bool> scheduleNotification({
required int id,
required String title,
required String body,
required DateTime scheduledDate,
String? channelId,
Map<String, dynamic>? payload,
List<NotificationAction>? actions,
bool allowWhileIdle = false,
})
Future<bool> scheduleRecurringNotification({
required int id,
required String title,
required String body,
required String repeatInterval,
required int hour,
required int minute,
String? channelId,
Map<String, dynamic>? payload,
List<NotificationAction>? actions,
})
Background Handling & Diagnostics
Future<void> configureBackgroundMessageHandler(
Future<void> Function(RemoteMessage message) handler,
)
static Future<void> handleBackgroundMessage(RemoteMessage message)
Future<NotificationDiagnosticsResult> runDiagnostics()
Future<void> setUnifiedMessageHandler(
Future<bool> Function(NormalizedMessage message, NotificationLifecycle lifecycle) handler,
)
Inbox Storage
fetch({int page = 0, int pageSize = 20})→Future<List<NotificationInboxItem>>upsert(NotificationInboxItem item)upsertAll(List<NotificationInboxItem> items)markRead(List<String> ids, {bool isRead = true})delete(List<String> ids)clear()count({bool unreadOnly = false})
Implementations:
InboxStorageService– SharedPreferences-backed persistence.InMemoryInboxStorage– memory-only, ideal for tests.
Badge Management
Future<void> setIOSBadgeCount(int count)
Future<int> getIOSBadgeCount()
Future<void> setAndroidBadgeCount(int count)
Future<int> getAndroidBadgeCount()
Future<void> clearBadgeCount()
Token Management
Future<String?> getFcmToken()
Future<void> clearToken()
Future<void> subscribeToTopic(String topic)
Future<void> unsubscribeFromTopic(String topic)
Future<void> unsubscribeFromAllTopics()
Analytics
void setAnalyticsCallback(Function(String, Map<String, dynamic>) callback)
void trackAnalyticsEvent(String event, Map<String, dynamic> data)
Data Models
NotificationData
class NotificationData {
final Map<String, dynamic> payload;
final String? title;
final String? body;
final String? imageUrl;
final String? icon;
final String? category;
final List<NotificationAction>? actions;
final DateTime? timestamp;
final NotificationTypeEnum type;
final bool isFromTerminated;
final String? messageId;
final String? senderId;
final int? badgeCount;
}
NotificationAction
class NotificationAction {
final String id;
final String title;
final bool destructive;
final Map<String, dynamic>? payload;
}
NotificationChannelData
class NotificationChannelData {
final String id;
final String name;
final String? description;
final String? groupId;
final NotificationImportanceEnum importance;
final bool playSound;
final String? soundPath;
final String? soundFileName;
final bool enableVibration;
final bool enableLights;
final Int64List? vibrationPattern;
final Color? ledColor;
final bool showBadge;
final NotificationPriorityEnum priority;
final List<NotificationAction>? actions;
}
Configuration
Notification Channels
Create custom notification channels for different types of notifications:
final List<NotificationChannelData> channels = [
NotificationChannelData(
id: 'default_channel',
name: 'Default Notifications',
description: 'Default notification channel',
importance: NotificationImportanceEnum.high,
priority: NotificationPriorityEnum.high,
playSound: true,
enableVibration: true,
enableLights: true,
),
NotificationChannelData(
id: 'silent_channel',
name: 'Silent Notifications',
description: 'Silent notification channel',
importance: NotificationImportanceEnum.low,
priority: NotificationPriorityEnum.low,
playSound: false,
enableVibration: false,
enableLights: false,
),
];
Platform-Specific Settings
Android
NotificationChannelData(
id: 'android_channel',
name: 'Android Notifications',
description: 'Android-specific notifications',
importance: NotificationImportanceEnum.max,
priority: NotificationPriorityEnum.max,
playSound: true,
enableVibration: true,
enableLights: true,
vibrationPattern: Int64List.fromList([0, 1000, 500, 1000]),
ledColor: Color(0xFFFF0000),
showBadge: true,
)
iOS
NotificationChannelData(
id: 'ios_channel',
name: 'iOS Notifications',
description: 'iOS-specific notifications',
importance: NotificationImportanceEnum.high,
priority: NotificationPriorityEnum.high,
playSound: true,
enableVibration: true,
enableLights: false,
showBadge: true,
)
Troubleshooting
Common Issues
Notifications not showing:
- Check Firebase configuration files are in place
- Verify sender ID is correct
- Check AndroidManifest.xml permissions
- Ensure notification channels are created
Scheduled notifications not working:
- Android 12+ (API 31+): Add
SCHEDULE_EXACT_ALARMandUSE_EXACT_ALARMpermissions - Android 13+ (API 33+): Request
SCHEDULE_EXACT_ALARMpermission at runtime - Ensure broadcast receivers are added to AndroidManifest.xml
- Check scheduled time is in the future
- Verify notification permissions are granted
Android 13+ Runtime Permission:
For Android 13+ devices, you need to request the exact alarm permission at runtime:
import 'package:permission_handler/permission_handler.dart';
// Request exact alarm permission (Android 13+)
if (Platform.isAndroid) {
final status = await Permission.scheduleExactAlarm.request();
if (status.isGranted) {
// Permission granted, you can schedule notifications
} else {
// Permission denied, handle gracefully
print('Exact alarm permission denied');
}
}
Permission-related issues:
"Exact alarms are not permitted"
- Cause: Missing
SCHEDULE_EXACT_ALARMpermission - Fix: Add permission to AndroidManifest.xml
- Alternative: Use
scheduleNotification()without exact timing
"No push notifications received"
- Cause: Missing
INTERNETorWAKE_LOCKpermission - Fix: Add basic permissions to AndroidManifest.xml
"Notifications don't vibrate"
- Cause: Missing
VIBRATEpermission - Fix: Add
VIBRATEpermission to AndroidManifest.xml
"Foreground notifications not showing"
- Cause: Missing
FOREGROUND_SERVICEpermission - Fix: Add
FOREGROUND_SERVICEpermission to AndroidManifest.xml
iOS badges not updating:
- Requires proper APNs certificate configuration
- May not work in simulator
- Must upload APNs key to Firebase Console
APNs token not set error:
- This is NORMAL until APNs is configured
- Generate APNs key in Apple Developer Console
- Upload
.p8key file to Firebase Console - Choose correct environment (Sandbox/Production)
- This is a Firebase requirement, not a plugin issue
Custom sounds not playing:
- Add sound files to correct platform directories
- Create notification channels before using sounds
- Check file permissions and formats
Analytics not tracking:
- Ensure analytics callback is set
- Check event names and data format
- Verify analytics service integration
Debug Mode
Enable debug mode for detailed logging:
// The plugin automatically logs detailed information in debug mode
// Check console output for initialization and operation logs
Error Handling
The plugin reports setup and runtime failures through logs, diagnostics, and returned values where possible:
try {
await FirebaseMessagingHandler.instance.init(
senderId: 'your_sender_id',
androidChannelList: channels,
androidNotificationIconPath: '@drawable/ic_notification',
);
} catch (e) {
print('Initialization failed: $e');
// Handle error appropriately
}
Contributing
Contributions are welcome. Please see the Contributing Guide for details.
Development Setup
- Fork the repository
- Create a feature branch
- Make your changes
- Add tests
- Submit a pull request
Code Style
- Follow Dart/Flutter conventions
- Add documentation for user-visible behavior
- Include unit tests
- Ensure backward compatibility
License
This project is licensed under the BSD 3-Clause License - see the LICENSE file for details.
Support
- Documentation: API Reference
- Guides: GitHub Pages docs
- Examples: Example App – guided FCM showcase experience
- Issues: GitHub Issues
What's Next?
Next up in the package
- Web polish - pre-permission explainer overlay plus deeper service-worker validation.
- macOS push validation - verify real token retrieval and foreground/background delivery on macOS hardware.
- Rich Android styles - big picture, inbox, progress, and media-style notifications.
- Permission Wizard v2 - rationale UI, exact-alarm guidance, provisional iOS flows, and richer result objects.
- Remote notification cancel - allow backend payloads to cancel local notifications by ID/group/channel.
Platform and delivery roadmap
- Desktop runtime validation - verify Windows/Linux local mode on real runners and document exact behavior.
- 6/6 platform support on pub.dev - use desktop declarations plus runtime validation to maximize platform credit.
- Publish pipeline hardening - finish external setup for Codecov and pub.dev publishing secrets/trusted publishing.
Documentation and growth
- README breakup - keep this file as the landing page and move deep walkthroughs into docs pages.
- API doc coverage - continue documenting the remaining exported surface to push toward full dartdoc coverage.
- Community launch - blog post, showcase GIFs, Flutter Gems, FlutterAwesome, and social launch.
Built for Flutter apps that need practical Firebase Messaging workflows without rebuilding the same notification plumbing every release.