# Mastering FCM in Flutter: Your Ultimate Guide to Engaging Notifications

## **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](http://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](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:

```yaml
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.](https://firebase.flutter.dev/docs/overview#initializing-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:

|  | 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`.

```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

```dart
@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:

```dart
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.

```dart
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](https://pub.dev/packages/flutter_local_notifications) package to help us.

### **Initialization**

Let's initialize `FlutterLocalNotificationsPlugin` in our `NotificationService`.

```dart
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.

```dart
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:

```dart
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

* [https://firebase.google.com/docs/cloud-messaging/flutter/client](https://firebase.google.com/docs/cloud-messaging/flutter/client)
    
* [https://firebase.google.com/docs/cloud-messaging/flutter/receive](https://firebase.google.com/docs/cloud-messaging/flutter/receive)
    
* [https://firebase.flutter.dev/docs/messaging/notifications/#foreground-notifications](https://firebase.flutter.dev/docs/messaging/notifications/#foreground-notifications)
