Flutter Interview Handbook

Push Notification Systems

100%

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 StatePush BehaviorPrimary API Handler
ForegroundApp is open; OS banner suppressed by defaultFirebaseMessaging.onMessage
BackgroundApp is minimized in background task switcherFirebaseMessaging.onBackgroundMessage
TerminatedApp process is killed/closedFirebaseMessaging.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.

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.onTokenRefresh listener to push updated tokens to your backend.
  • iOS APNs Authentication Keys (.p8): Use Apple APNs .p8 Authentication Keys instead of legacy .p12 certificates on Firebase Console to avoid annual certificate expiration outages.