Push Notifications

Medium PriorityAsked in ~55% of senior interviews

4 min read

Platform Integration

Payload \ App stateForegroundBackgroundTerminated
notification: onlyonMessage (no system UI; you must show local notification)System banner; tap → onMessageOpenedAppSystem banner; tap launches app → getInitialMessage
data: only (silent)onMessage (no banner unless you show one)onBackgroundMessage (limited time)OS may run onBackgroundMessage with limits
BothBoth behaviours combine (system banner + your handler)System banner with the notification payload; data accessible on tapSame

The two payload types matter: if you want full control over how it looks (custom layout, image, action buttons), use data-only + flutter_local_notifications. If you're OK with system defaults, use notification: payload.


Code in action

// MUST be top-level + AOT-safe
@pragma('vm:entry-point')
Future<void> onBackgroundMessage(RemoteMessage msg) async {
  await Firebase.initializeApp();
  await DataSync.handle(msg.data);                  // limited window — be fast
}

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();
  FirebaseMessaging.onBackgroundMessage(onBackgroundMessage);
  runApp(const MyApp());
}

class NotificationService {
  final _fcm = FirebaseMessaging.instance;

  Future<void> init() async {
    // 1. Permission (iOS + Android 13+)
    final settings = await _fcm.requestPermission();
    if (settings.authorizationStatus != AuthorizationStatus.authorized) return;

    // 2. Token (send to your server, refresh on rotation)
    final token = await _fcm.getToken();
    await api.registerDevice(token);
    _fcm.onTokenRefresh.listen(api.registerDevice);

    // 3. Foreground messages — show your own UI
    FirebaseMessaging.onMessage.listen((msg) {
      LocalNotifications.show(
        title: msg.notification?.title ?? '',
        body:  msg.notification?.body  ?? '',
        payload: msg.data,
      );
    });

    // 4. Tapped in background → app was alive in background
    FirebaseMessaging.onMessageOpenedApp.listen(_handleTap);

    // 5. Tapped while terminated → app was relaunched
    final initial = await _fcm.getInitialMessage();
    if (initial != null) _handleTap(initial);
  }

  void _handleTap(RemoteMessage msg) {
    final route = msg.data['route'] as String?;
    if (route != null) router.go(route);            // deep link
  }
}

Best practices

PracticeWhy
Request permission at the right moment, not on launchHigher grant rates; iOS HIG guidance
Show foreground notifications via local notificationsFCM doesn't show banner in foreground by default
Deep link from notification → use data payload with route infoLand users on the right screen
Re-send token on refresh and on logout/loginToken can change; ensure server has the right one
Use topics for broadcast, individual tokens for personalTopics scale; tokens target
Server-side: send apns-priority: 10, priority: high for user-facingLower priority = OS may delay or batch
iOS: configure background modes ("Remote notifications")Without it, silent push won't wake the app
Use notification channels on Android 8+ for categorisationUser can mute categories independently

Common mistakes to avoid

❌ Expecting the system to show a banner in foreground
   It doesn't (by default). Show your own via flutter_local_notifications.

❌ Calling Navigator from a background callback
   No tree exists. Persist intent (shared prefs / pending nav state) and
   navigate when the app comes to foreground.

❌ Treating delivery as guaranteed
   Both FCM and APNs are best-effort. For mission-critical events,
   pair push with a server-side fallback (sync on open).

❌ Hard-coding token expectations
   Tokens rotate (reinstall, restore from backup, language change).
   Listen to onTokenRefresh, always.

❌ Forgetting onBackgroundMessage requires AOT entry point
   @pragma('vm:entry-point') — without it, release builds fail silently.

❌ Sending PII in the visible notification body
   "John Smith paid you $50" shows on the lock screen.
   Use less specific text + open the app for details.

❌ Not testing terminated-state taps
   getInitialMessage is easy to forget. Test by force-quitting the app
   and tapping a fresh notification.

Interview follow-ups

  1. What's the difference between notification: and data: payloads? notification: is a structured payload (title, body, icon) that the system displays automatically. data: is opaque key/value pairs your app processes. Use data-only for full control (custom UI, silent sync, deep link intents). Most production apps send both — system handles display, data handles routing.

  2. How do you deep-link from a notification tap? Include a route (or similar) in the data: payload. In onMessageOpenedApp and getInitialMessage, parse it and call router.go(route). For terminated state, you may need to wait for app init before navigating — show a splash, then deep link.

  3. What's the difference between FCM and APNs? FCM is Google's cross-platform service; APNs is Apple's. FCM on iOS delegates to APNs under the hood — you still configure APNs credentials in Firebase. Same on Android except FCM is native. From Flutter's perspective, firebase_messaging abstracts both.

  4. How do you handle notification preferences per user? Server-side: store preference flags, only push when the user has opted in. Client-side: use FCM topics (subscribe/unsubscribe based on prefs) plus Android notification channels (let the user mute categories at the OS level). Always provide an in-app settings screen.


How helpful was this content?

Please sign in to rate this article.