Mastering FCM in Flutter: Your Ultimate Guide to Engaging Notifications
Elevate Your Flutter App's Notification Game: Master Firebase Cloud Messaging Like a Pro!
Introduction
Hey there, fellow Flutter enthusiast! Are you ready to level up your app's notification game? Buckle up because we're about to dive into the world of Firebase Cloud Messaging (FCM) and Flutter. In this guide, we'll explore how to set up FCM for your Flutter app, utilizing the power of Firebase and the magic of Dart. By the end of this journey, you'll be a notification ninja, delivering timely and engaging messages to your users like a pro.
Getting Started with Firebase Cloud Messaging
What is Firebase Cloud Messaging?
Firebase Cloud Messaging, or FCM for short, is a cross-platform messaging solution that allows you to send notifications and data messages to your users on Android, iOS, and the web. It's a powerful tool that enables you to engage with your audience in real time, keeping them informed and connected to your app.
Setting up Firebase for your Flutter App
First things first, let's set up Firebase for our Flutter app. Follow these steps:
Create a new Flutter project or open an existing one.
Head over to the Firebase Console (console.firebase.google.com) and create a new project.
Follow the on-screen instructions to add your Flutter app to the Firebase project.
Download the
google-services.json
file for Android orGoogleService-Info.plist
file for iOS and place it in the respective directories of your Flutter app.
You can read more about the FCM setup here: https://firebase.google.com/docs/cloud-messaging/flutter/client
Dependency Setup
To use Firebase Cloud Messaging in your Flutter app, you need to add the firebase_messaging
package to your pubspec.yaml
file:
dependencies:
flutter:
sdk: flutter
firebase_messaging: ^14.7.2
flutter_local_notifications: ^16.1.0
Run flutter pub get
to install the packages.
Note: Before using Firebase Cloud Messaging, you must first have ensured you have initialized FlutterFire.
Initializing Firebase Messaging
Before we move on to implementing Firebase Messaging in our app, we must look at understand different message types:
Message types
A message payload can be viewed as one of three types:
Notification-only message: The payload contains a
notification
property, which will be used to present a visible notification to the user.Data-only message: Also known as a "silent message", this payload contains custom key/value pairs within the
data
property which can be used how you see fit. These messages are considered "low priority".Notification & Data messages: Payloads with both
notification
anddata
properties.
Based on your application's current state, incoming payloads require different implementations to handle them:
Foreground | Background | Terminated | |
Notification | onMessage | onBackgroundMessage | onBackgroundMessage |
Data | onMessage | onBackgroundMessage | onBackgroundMessage |
Notification & Data | onMessage | onBackgroundMessage | onBackgroundMessage |
We'll dive into the implementation part soon!
Note: Data-only messages are considered low priority by devices when your application is in the background or terminated, and will be ignored. You can however explicitly increase the priority by sending additional properties on the FCM payload:
On Android, set the
priority
field tohigh
.On Apple (iOS & macOS), set the
content-available
field totrue
.
Let's initialize Firebase Messaging in our app. We'll create a NotificationService
class to handle all things related to notifications in a file named notification_service.dart
.
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
class NotificationService {
final FirebaseMessaging _firebaseMessaging = FirebaseMessaging.instance;
final FlutterLocalNotificationsPlugin _flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
String fcmToken = '';
NotificationService() {
// Initialize Firebase Messaging
_initializeFCM();
// Initialize Flutter Local Notifications
_initializeFlutterLocalNotifications();
}
void _initializeFCM() async {
_firebaseMessaging.setAutoInitEnabled(true);
// Request permission
_firebaseMessaging.requestPermission(sound: true, badge: true, alert: true);
fcmToken = await _firebaseMessaging.getToken() ?? '';
debugPrint('FCM Token: $fcmToken');
// Handle background messages
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
// Handle messages when the app is in the foreground
FirebaseMessaging.onMessage.listen((RemoteMessage message) async {
if (message.notification != null) {
showLocalNotification(message);
}
});
}
void _initializeFlutterLocalNotifications() {
// Initialize Flutter Local Notifications
// We'll add code here later...
}
void showLocalNotification(RemoteMessage message) {
// Show local notification
// We'll add code here later...
}
}
NotificationService notificationService = NotificationService();
Handling messages whilst your application is in the background is a little different. Messages can be handled via the onBackgroundMessage handler. When received, an isolate is spawned (Android only, iOS/macOS does not require a separate isolate) allowing you to handle messages even when your application is not running.
There are a few things to keep in mind about your background message handler:
It must not be an anonymous function.
It must be a top-level function (e.g. not a class method which requires initialization).
Hence, add the following code at the top of notification_service.dart
, outside the NotificationService
class
@pragma('vm:entry-point')
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
debugPrint(message.toMap().toString());
notificationService.showLocalNotification(message);
}
Handling Notification On-Tap Action
We also need to handle on-tap action on our notification banners when the app is in the foreground, background and when it's launched from a terminated state. So, in the same class NotificationService
add the following:
class NotificationService {
// ...
void handlePendingNotifications() {
// Handle initial notification when the app is started from a terminated state
_firebaseMessaging.getInitialMessage().then((message) {
if (message != null) {
_handleNotificationTap(message);
}
});
}
void _handleNotificationTap(RemoteMessage message) {
// Add your notification tap handling logic
}
}
To handle the notification tap when the app is starting up from a terminated state, you must call the handlePendingNotifications()
method once when your app starts up. You can do so in initState()
your HomeScreen Widget which gets initialized at the beginning of the app.
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
@override
void initState() {
// Handle the initial notification when the app is started from a terminated state
notificationService.handlePendingNotifications();
super.initState();
}
// ...
}
Configuring Local Notifications
Notification messages which arrive whilst the application is in the foreground will not display a visible notification by default, on both Android & iOS. It is, however, possible to override this behavior:
On Android, notification messages are sent to Notification Channels which are used to control how a notification is delivered. The default FCM channel used is hidden from users, however provides a "default" importance level. Heads up notifications (shown when our app is in the foreground) require a "max" importance level.
This means that we need to first create a new channel with a maximum importance level & then assign incoming FCM notifications to this channel. Although this is outside of the scope of FlutterFire, we can take advantage of the flutter_local_notifications package to help us.
Initialization
Let's initialize FlutterLocalNotificationsPlugin
in our NotificationService
.
void _initializeFlutterLocalNotifications() {
var initializationSettingsAndroid = const AndroidInitializationSettings('@drawable/ic_launcher');
var initializationSettingsIOS = DarwinInitializationSettings(
onDidReceiveLocalNotification: (id, title, body, payload) async {},
);
var initializationSettings = InitializationSettings(
android: initializationSettingsAndroid,
iOS: initializationSettingsIOS,
);
_flutterLocalNotificationsPlugin.initialize(
initializationSettings,
onDidReceiveNotificationResponse: (details) {
final message = RemoteMessage.fromMap({'data': jsonDecode(details.payload ?? '{}')});
_handleNotificationTap(message);
},
);
_flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>()
?.requestNotificationsPermission();
_flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation<IOSFlutterLocalNotificationsPlugin>()
?.requestPermissions(
alert: true,
badge: true,
sound: true,
);
}
Showing Local Notifications
Finally, let's define a method to show local notifications inside the NotificationService
class.
void showLocalNotification(RemoteMessage message) {
var androidPlatformChannelSpecifics = const AndroidNotificationDetails(
'channel_id',
'Channel Name',
channelDescription: 'Channel Description',
importance: Importance.max,
priority: Priority.max,
);
var iOSPlatformChannelSpecifics = const DarwinNotificationDetails();
var platformChannelSpecifics = NotificationDetails(
android: androidPlatformChannelSpecifics,
iOS: iOSPlatformChannelSpecifics,
);
// Define the ID of the notification
final id = DateTime.now().millisecondsSinceEpoch ~/ 1000;
_flutterLocalNotificationsPlugin.show(
id,
message.notification?.title ?? '',
message.notification?.body ?? '',
platformChannelSpecifics,
payload: jsonEncode(message.data),
);
}
Final Showdown
Here's the complete code example of our Notification Service:
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
// Top level background notification handler
@pragma('vm:entry-point')
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
debugPrint(message.toMap().toString());
notificationService.showLocalNotification(message);
}
class NotificationService {
final FirebaseMessaging _firebaseMessaging = FirebaseMessaging.instance;
final FlutterLocalNotificationsPlugin _flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
String fcmToken = '';
NotificationService() {
// Initialize Firebase Messaging
_initializeFCM();
// Initialize Flutter Local Notifications
_initializeFlutterLocalNotifications();
}
void _initializeFCM() async {
_firebaseMessaging.setAutoInitEnabled(true);
// Request permission
_firebaseMessaging.requestPermission(sound: true, badge: true, alert: true);
fcmToken = await _firebaseMessaging.getToken() ?? '';
debugPrint('FCM Token: $fcmToken');
// Handle background messages
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
// Handle messages when the app is in the foreground
FirebaseMessaging.onMessage.listen((RemoteMessage message) async {
if (message.notification != null) {
showLocalNotification(message);
}
});
}
void handlePendingNotifications() {
// Handle initial notification when the app is started from a terminated state
_firebaseMessaging.getInitialMessage().then((message) {
if (message != null) {
_handleNotificationTap(message);
}
});
}
void _handleNotificationTap(RemoteMessage message) {
// Add your notification tap handling logic
}
void _initializeFlutterLocalNotifications() {
var initializationSettingsAndroid = const AndroidInitializationSettings('@drawable/ic_launcher');
var initializationSettingsIOS = DarwinInitializationSettings(
onDidReceiveLocalNotification: (id, title, body, payload) async {},
);
var initializationSettings = InitializationSettings(
android: initializationSettingsAndroid,
iOS: initializationSettingsIOS,
);
_flutterLocalNotificationsPlugin.initialize(
initializationSettings,
onDidReceiveNotificationResponse: (details) {
final message = RemoteMessage.fromMap({'data': jsonDecode(details.payload ?? '{}')});
_handleNotificationTap(message);
},
);
_flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>()
?.requestNotificationsPermission();
_flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation<IOSFlutterLocalNotificationsPlugin>()
?.requestPermissions(
alert: true,
badge: true,
sound: true,
);
}
void showLocalNotification(RemoteMessage message) {
var androidPlatformChannelSpecifics = const AndroidNotificationDetails(
'channel_id',
'Channel Name',
channelDescription: 'Channel Description',
importance: Importance.max,
priority: Priority.max,
);
var iOSPlatformChannelSpecifics = const DarwinNotificationDetails();
var platformChannelSpecifics = NotificationDetails(
android: androidPlatformChannelSpecifics,
iOS: iOSPlatformChannelSpecifics,
);
// Define the ID of the notification
final id = DateTime.now().millisecondsSinceEpoch ~/ 1000;
_flutterLocalNotificationsPlugin.show(
id,
message.notification?.title ?? '',
message.notification?.body ?? '',
platformChannelSpecifics,
payload: jsonEncode(message.data),
);
}
}
NotificationService notificationService = NotificationService();
Wrapping Up and Next Steps
Congratulations! You've successfully set up Firebase Cloud Messaging for your Flutter app. Now, you can send notifications to your users and keep them engaged with your app's content. But remember, this is just the beginning. There's so much more you can do with FCM, like sending data messages, handling user interactions, and targeting specific user segments.
Happy Coding! ๐