Mastering FCM in Flutter: Your Ultimate Guide to Engaging Notifications

Mastering FCM in Flutter: Your Ultimate Guide to Engaging Notifications

Elevate Your Flutter App's Notification Game: Master Firebase Cloud Messaging Like a Pro!

ยท

8 min read

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:

  1. Create a new Flutter project or open an existing one.

  2. Head over to the Firebase Console (console.firebase.google.com) and create a new project.

  3. Follow the on-screen instructions to add your Flutter app to the Firebase project.

  4. 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:

  1. Notification-only message: The payload contains a notification property, which will be used to present a visible notification to the user.

  2. 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".

  3. 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:

ForegroundBackgroundTerminated
NotificationonMessageonBackgroundMessageonBackgroundMessage
DataonMessageonBackgroundMessageonBackgroundMessage
Notification & DataonMessageonBackgroundMessageonBackgroundMessage

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() {
    // 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! ๐Ÿš€


References

ย