Push Notification Systems
Push notifications are critical for user re-engagement, transactional alerts, and real-time background sync. Flutter integrates with Firebase Cloud Messaging (FCM) and Apple Push Notification service (APNs) using firebase_messaging and flutter_local_notifications.
1. End-to-End Push Notification Architecture
βββββββββββββββββ 1. Send Notification API ββββββββββββββββββββ
β Backend Serverβ ββββββββββββββββββββββββββββββββββββββββββββββββββββββββΊ β Firebase / APNs β
βββββββββββββββββ (Token: device_fcm_token_123, Payload: {...}) ββββββββββ¬ββββββββββ
β² β
β 2. Register FCM Token β 3. Push Over TCP Socket
β βΌ
βββββββββ΄ββββββββ ββββββββββββββββββββ
β Mobile Device β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β Mobile OS Engine β
βββββββββββββββββ 4. Trigger Background Isolate ββββββββββββββββββββ3 App Lifecycle Execution States
| App State | Push Behavior | Primary API Handler |
|---|---|---|
| Foreground | App is open; OS banner suppressed by default | FirebaseMessaging.onMessage |
| Background | App is minimized in background task switcher | FirebaseMessaging.onBackgroundMessage |
| Terminated | App process is killed/closed | FirebaseMessaging.instance.getInitialMessage() |
2. Background Isolate Entry Point (@pragma('vm:entry-point'))
When a push notification arrives while the app is in a Background or Terminated state, the operating system spawns a new background Dart Isolate to run the background handler.
The AOT Tree-Shaking Hazard
In release mode, Dartβs AOT compiler performs static dead-code tree shaking. If a top-level function is not referenced directly in main(), the compiler strips it from the compiled binary!
- Fix: Annotate the top-level background handler with
@pragma('vm:entry-point')to instruct the AOT compiler to preserve the function.
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
// Mandatory pragma annotation prevents AOT tree-shaking!
@pragma('vm:entry-point')
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
await Firebase.initializeApp();
print("Handling a background message: ${message.messageId}");
}
void main() {
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
runApp(const MyApp());
}3. Display vs. Silent (Data-Only) Notifications
1. Display Notifications
Contains notification key (title, body). The OS automatically displays a banner if the app is in the background or terminated.
2. Silent (Data-Only) Notifications
Contains ONLY the data key without a notification block.
- Use Case: Silent background sync (e.g. updating local database or pre-fetching news articles).
- OS Constraint: Both iOS and Android throttle silent notifications if sent too frequently to conserve battery.
4. Deep-Link Navigation Handling Across App States
class NotificationRouter {
Future<void> setupInteractions(BuildContext context) async {
// 1. App killed/terminated state tap handler
RemoteMessage? initialMessage = await FirebaseMessaging.instance.getInitialMessage();
if (initialMessage != null) {
_handleMessageNavigation(context, initialMessage);
}
// 2. Background state tap handler
FirebaseMessaging.onMessageOpenedApp.listen((message) {
_handleMessageNavigation(context, message);
});
}
void _handleMessageNavigation(BuildContext context, RemoteMessage message) {
final route = message.data['route'];
if (route != null) {
Navigator.of(context).pushNamed(route);
}
}
}5. Trade-offs & Production Considerations
- FCM Token Refresh Management: FCM tokens refresh when the app restores from backup, re-installs, or clears data. Always register a
FirebaseMessaging.instance.onTokenRefreshlistener to push updated tokens to your backend. - iOS APNs Authentication Keys (.p8): Use Apple APNs
.p8Authentication Keys instead of legacy.p12certificates on Firebase Console to avoid annual certificate expiration outages.