Handling FCM in the Foreground
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.
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?withtitleandbodystrings. 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).
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
StreamSubscriptionindisposeto avoid processing messages after the widget is unmounted. - Use a global
ScaffoldMessengerKeyor 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
setStatedirectly inside the message handler unless you are certain the widget is still mounted — always guard withif (mounted). - Deduplicate using
message.messageIdif your server may send duplicates.
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.