Push Notifications & Background Services

Handling FCM in the Foreground

16 min Lesson 2 of 11

Handling FCM in the Foreground

When your Flutter app is open and in the foreground, Firebase Cloud Messaging (FCM) delivers messages silently — the OS does not display a system notification banner. Instead, your app receives the message through the FirebaseMessaging.onMessage stream, and it is entirely your responsibility to decide how to present that information to the user. This lesson covers how to listen for incoming messages, parse the RemoteMessage payload, and show a meaningful in-app alert.

Note: FCM distinguishes three app states — foreground, background, and terminated. The onMessage stream only fires when the app is in the foreground. Background and terminated states use different handlers covered in later lessons.

Setting Up the onMessage Stream

The FirebaseMessaging.onMessage property returns a Stream<RemoteMessage>. You subscribe to it inside initState of a long-lived widget (typically your root widget) and cancel the subscription in dispose to prevent memory leaks.

Subscribing to onMessage in initState

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

class AppRoot extends StatefulWidget {
  const AppRoot({super.key});

  @override
  State<AppRoot> createState() => _AppRootState();
}

class _AppRootState extends State<AppRoot> {
  // Hold the StreamSubscription so we can cancel it later
  late final StreamSubscription<RemoteMessage> _messageSubscription;

  @override
  void initState() {
    super.initState();
    // Listen for foreground FCM messages
    _messageSubscription = FirebaseMessaging.onMessage.listen(_handleForegroundMessage);
  }

  void _handleForegroundMessage(RemoteMessage message) {
    // Invoked on the main thread whenever a message arrives in the foreground
    debugPrint('Foreground message received: ${message.messageId}');
    _showInAppAlert(message);
  }

  @override
  void dispose() {
    _messageSubscription.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(home: HomeScreen());
  }
}

Parsing the RemoteMessage Payload

A RemoteMessage object contains several important fields you should know how to access:

  • message.notification — a RemoteNotification? with title and body strings. Present only when the server sends a notification payload.
  • message.data — a Map<String, String> containing arbitrary key-value pairs set by your backend. Always present (may be empty).
  • message.messageId — a unique identifier for deduplication.
  • message.sentTime — the DateTime? when the server sent the message.
  • message.from — the sender (topic or FCM registration token).
Tip: Always null-check message.notification before accessing its fields. Pure data messages (sent with no notification block) will have a null notification but a populated data map.

Parsing Notification and Data Fields

void _handleForegroundMessage(RemoteMessage message) {
  // Safely extract the notification fields
  final String title = message.notification?.title ?? 'New Notification';
  final String body  = message.notification?.body  ?? '';

  // Extract custom data sent from your backend
  final String? type     = message.data['type'];
  final String? targetId = message.data['target_id'];

  debugPrint('Title: $title');
  debugPrint('Body: $body');
  debugPrint('Type: $type, TargetId: $targetId');

  // Route based on the custom data type
  if (type == 'chat') {
    _showChatAlert(title, body, targetId);
  } else if (type == 'promo') {
    _showPromoAlert(title, body);
  } else {
    _showGenericAlert(title, body);
  }
}

Showing an In-App Alert with a SnackBar

The simplest way to surface a foreground notification is a SnackBar. It is non-intrusive, familiar to users, and easy to implement. You need a valid BuildContext with a Scaffold ancestor, or use a global ScaffoldMessengerKey for access from outside the widget tree.

Displaying a SnackBar for Foreground Messages

// Declare a global key at the top level (e.g., in main.dart)
final GlobalKey<ScaffoldMessengerState> scaffoldMessengerKey =
    GlobalKey<ScaffoldMessengerState>();

// Pass it to MaterialApp
MaterialApp(
  scaffoldMessengerKey: scaffoldMessengerKey,
  home: const HomeScreen(),
);

// Then call it from anywhere, including outside the widget tree:
void _showInAppAlert(RemoteMessage message) {
  final String title = message.notification?.title ?? 'Notification';
  final String body  = message.notification?.body  ?? '';

  scaffoldMessengerKey.currentState?.showSnackBar(
    SnackBar(
      content: Column(
        mainAxisSize: MainAxisSize.min,
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(title, style: const TextStyle(fontWeight: FontWeight.bold)),
          if (body.isNotEmpty) Text(body),
        ],
      ),
      duration: const Duration(seconds: 4),
      behavior: SnackBarBehavior.floating,
      action: SnackBarAction(
        label: 'View',
        onPressed: () => _handleNotificationTap(message),
      ),
    ),
  );
}

Showing a Dialog Alert Instead

For higher-priority messages you may prefer a modal dialog. Use the same global key pattern to obtain the overlay context, or call showDialog from within the widget if you have a local context available.

Modal Dialog for Critical Foreground Notifications

void _showCriticalAlert(BuildContext context, RemoteMessage message) {
  showDialog<void>(
    context: context,
    barrierDismissible: false,
    builder: (_) => AlertDialog(
      title: Text(message.notification?.title ?? 'Alert'),
      content: Text(message.notification?.body ?? ''),
      actions: [
        TextButton(
          onPressed: () => Navigator.of(context).pop(),
          child: const Text('Dismiss'),
        ),
        ElevatedButton(
          onPressed: () {
            Navigator.of(context).pop();
            _handleNotificationTap(message);
          },
          child: const Text('Open'),
        ),
      ],
    ),
  );
}

Best Practices for Foreground Handling

  • Always cancel your StreamSubscription in dispose to avoid processing messages after the widget is unmounted.
  • Use a global ScaffoldMessengerKey or navigator key so the handler can present UI regardless of which screen is currently active.
  • Differentiate by the data['type'] field to route different notification categories (chat, order update, promo) to the correct screen.
  • Avoid calling setState directly inside the message handler unless you are certain the widget is still mounted — always guard with if (mounted).
  • Deduplicate using message.messageId if your server may send duplicates.
Warning: Do not rely on message.notification for business logic. On Android, FCM may suppress the notification field under certain conditions when the app is in the foreground. Always encode actionable data in message.data and treat the notification fields as display hints only.

Summary

To handle FCM messages while your app is in the foreground: listen to FirebaseMessaging.onMessage in initState, cancel the subscription in dispose, null-safely parse message.notification for display text and message.data for routing logic, then present the alert via a SnackBar (for low-priority) or a Dialog (for high-priority) using a global ScaffoldMessengerKey so any screen can surface the notification regardless of the current navigation state.