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 or GoogleService-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 and data 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 to high.
On Apple (iOS & macOS), set the content-available field to true.
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() {
_initializeFCM();
_initializeFlutterLocalNotifications();
}
void _initializeFCM() async {
_firebaseMessaging.setAutoInitEnabled(true);
_firebaseMessaging.requestPermission(sound: true, badge: true, alert: true);
fcmToken = await _firebaseMessaging.getToken() ?? '';
debugPrint('FCM Token: $fcmToken');
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
FirebaseMessaging.onMessage.listen((RemoteMessage message) async {
if (message.notification != null) {
showLocalNotification(message);
}
});
}
void _initializeFlutterLocalNotifications() {
}
void showLocalNotification(RemoteMessage message) {
}
}
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:
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() {
_firebaseMessaging.getInitialMessage().then((message) {
if (message != null) {
_handleNotificationTap(message);
}
});
}
void _handleNotificationTap(RemoteMessage message) {
}
}
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() {
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,
);
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';
@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() {
_initializeFCM();
_initializeFlutterLocalNotifications();
}
void _initializeFCM() async {
_firebaseMessaging.setAutoInitEnabled(true);
_firebaseMessaging.requestPermission(sound: true, badge: true, alert: true);
fcmToken = await _firebaseMessaging.getToken() ?? '';
debugPrint('FCM Token: $fcmToken');
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
FirebaseMessaging.onMessage.listen((RemoteMessage message) async {
if (message.notification != null) {
showLocalNotification(message);
}
});
}
void handlePendingNotifications() {
_firebaseMessaging.getInitialMessage().then((message) {
if (message != null) {
_handleNotificationTap(message);
}
});
}
void _handleNotificationTap(RemoteMessage message) {
}
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,
);
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! 🚀
References