Handling Different Types of Push Notifications, Including Data and Notification Messages in Flutter

Push notifications are a powerful tool for engaging users, delivering timely information, and enhancing the overall user experience in mobile applications. Flutter, with its rich set of libraries and tools, provides excellent support for handling push notifications. Understanding the different types of push notifications and how to manage them in Flutter is essential for building robust and user-friendly apps. This blog post delves into the nuances of handling various push notification types, including data and notification messages, within a Flutter environment.

What are Push Notifications?

Push notifications are messages that are ‘pushed’ from a server to a user’s mobile device. They can appear as a banner, badge, or alert, even when the user isn’t actively using the application. There are typically two main types of push notifications:

  • Notification Messages: These are handled directly by the Firebase Cloud Messaging (FCM) SDK. They automatically display a notification to the user if the app is in the background.
  • Data Messages: These are handled by the application code. They don’t automatically display a notification, giving developers more control over how the notification is presented and processed.

Why Handle Different Types of Push Notifications?

  • Flexibility: Handling both notification and data messages provides more flexibility in how you deliver and process notifications.
  • Customization: Data messages allow you to build custom notification experiences.
  • Background Processing: Data messages can be used to trigger background tasks in the app.
  • Engagement: Properly handled notifications improve user engagement and retention.

Setting Up Firebase Cloud Messaging (FCM) in Flutter

Before you can handle push notifications, you need to set up Firebase Cloud Messaging (FCM) in your Flutter project.

Step 1: Create a Firebase Project

Go to the Firebase Console and create a new project or select an existing one.

Step 2: Add Firebase to Your Flutter App

Add a new Firebase app for Android and iOS, following the instructions in the Firebase console. This will involve downloading configuration files (google-services.json for Android and GoogleService-Info.plist for iOS) and adding them to your project.

Step 3: Add Firebase Dependencies to Your Flutter Project

In your pubspec.yaml file, add the necessary Firebase dependencies:

dependencies:
  firebase_core: ^2.15.0
  firebase_messaging: ^14.6.0

Run flutter pub get to install the dependencies.

Step 4: Configure Android and iOS Projects

Follow the Firebase documentation to configure your Android and iOS projects with the downloaded configuration files and any required settings.

Handling Notification Messages

Notification messages are straightforward to handle. The FCM SDK automatically displays the notification if the app is in the background. However, you might want to customize how the notification appears when the app is in the foreground.

Receiving Notification Messages in the Foreground

To handle notification messages in the foreground, you can use the FirebaseMessaging.onMessage stream:

import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/material.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();
  FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
  runApp(MyApp());
}

@pragma('vm:entry-point')
Future _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
  await Firebase.initializeApp();
  print("Handling a background message: ${message.messageId}");
}


class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State {

  @override
  void initState() {
    super.initState();
    
    FirebaseMessaging.instance.getToken().then((token) {
      print("FCM Token: $token");
    });

    FirebaseMessaging.onMessage.listen((RemoteMessage message) {
      print('Got a message whilst in the foreground!');
      print('Message data: ${message.data}');

      if (message.notification != null) {
        print('Message also contained a notification: ${message.notification!.body}');
        // Show the notification using a local notification plugin or a dialog
        showDialog(
          context: context,
          builder: (BuildContext context) => AlertDialog(
            title: Text(message.notification!.title ?? 'Notification'),
            content: Text(message.notification!.body ?? 'No body'),
            actions: [
              TextButton(
                child: Text('OK'),
                onPressed: () {
                  Navigator.of(context).pop();
                },
              ),
            ],
          ),
        );
      }
    });

    FirebaseMessaging.onMessageOpenedApp.listen((message) {
      print('Message opened app: ${message.notification?.body}');
      // Navigate to a specific screen based on the notification
    });

    FirebaseMessaging.instance
    .getInitialMessage()
    .then((RemoteMessage? message) {
      if (message != null) {
        print('Opened from terminated state: ${message.notification?.body}');
        //Handle initial message here
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('Flutter FCM Demo'),
        ),
        body: Center(
          child: Text('Check your notifications!'),
        ),
      ),
    );
  }
}

In this example:

  • We listen to the FirebaseMessaging.onMessage stream.
  • When a notification is received, we display it using an AlertDialog.

Handling Data Messages

Data messages give you more control over how notifications are handled. They don’t automatically display a notification. Instead, your app code must process the message and decide what to do with it.

Receiving Data Messages

To handle data messages, use the same FirebaseMessaging.onMessage stream as with notification messages, but this time, focus on the message.data payload:

FirebaseMessaging.onMessage.listen((RemoteMessage message) {
  print('Got a data message whilst in the foreground!');
  print('Message data: ${message.data}');

  // Process the data message
  if (message.data.isNotEmpty) {
    String? title = message.data['title'];
    String? body = message.data['body'];

    // Display a custom notification
    showDialog(
      context: context,
      builder: (BuildContext context) => AlertDialog(
        title: Text(title ?? 'Notification'),
        content: Text(body ?? 'No body'),
        actions: [
          TextButton(
            child: Text('OK'),
            onPressed: () {
              Navigator.of(context).pop();
            },
          ),
        ],
      ),
    );
  }
});

Here, we extract the title and body from the message.data and display a custom notification. You can perform any action with the data, such as updating the UI, storing data locally, or triggering a background task.

Handling Background Data Messages

To handle data messages when the app is in the background or terminated, you can use the FirebaseMessaging.onBackgroundMessage handler. This handler must be a top-level function and annotated with @pragma('vm:entry-point'):

@pragma('vm:entry-point')
Future _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
  await Firebase.initializeApp();
  print("Handling a background message: ${message.messageId}");
  print('Message data: ${message.data}');

  // Process the data message
  if (message.data.isNotEmpty) {
    String? title = message.data['title'];
    String? body = message.data['body'];

    // You can't update UI here directly
    // Use a local notification plugin to display a notification
    // or perform other background tasks
  }
}

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();
  FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
  runApp(MyApp());
}

Inside the _firebaseMessagingBackgroundHandler, you can perform various tasks, but updating the UI directly is not possible. You typically use a local notification plugin (e.g., flutter_local_notifications) to display a notification or perform background tasks.

Example: Using flutter_local_notifications

To display notifications received in the background, you can integrate the flutter_local_notifications plugin.

Step 1: Add the Dependency

Add the flutter_local_notifications dependency to your pubspec.yaml file:

dependencies:
  flutter_local_notifications: ^16.1.0

Run flutter pub get to install the dependency.

Step 2: Initialize the Plugin

import 'package:flutter_local_notifications/flutter_local_notifications.dart';

final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
    FlutterLocalNotificationsPlugin();

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();

  // Initialize local notifications
  const AndroidInitializationSettings initializationSettingsAndroid =
      AndroidInitializationSettings('app_icon'); // replace 'app_icon' with your notification icon
  final InitializationSettings initializationSettings =
      InitializationSettings(android: initializationSettingsAndroid);
  await flutterLocalNotificationsPlugin.initialize(initializationSettings);

  FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
  runApp(MyApp());
}

Step 3: Display Local Notifications

import 'package:flutter_local_notifications/flutter_local_notifications.dart';

@pragma('vm:entry-point')
Future _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
  await Firebase.initializeApp();
  print("Handling a background message: ${message.messageId}");
  print('Message data: ${message.data}');

  if (message.data.isNotEmpty) {
    String? title = message.data['title'];
    String? body = message.data['body'];

    // Show a local notification
    const AndroidNotificationDetails androidPlatformChannelSpecifics =
        AndroidNotificationDetails(
      'your_channel_id',
      'your_channel_name',
      channelDescription: 'Your channel description',
      importance: Importance.max,
      priority: Priority.high,
      ticker: 'ticker',
    );
    const NotificationDetails platformChannelSpecifics =
        NotificationDetails(android: androidPlatformChannelSpecifics);
    await flutterLocalNotificationsPlugin.show(
        0, title, body, platformChannelSpecifics,
        payload: 'item x');
  }
}

In this setup, when a data message is received in the background, a local notification is displayed using the flutter_local_notifications plugin.

Handling Notification Interactions

You may want to perform certain actions when the user taps on a notification, such as navigating to a specific screen or opening a particular resource within the app.

Handling Notification Taps

You can use the FirebaseMessaging.onMessageOpenedApp stream to handle taps on notifications when the app is in the background or terminated.

FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
  print('Message opened app: ${message.notification?.body}');
  // Navigate to a specific screen based on the notification
  if (message.data['type'] == 'chat') {
    Navigator.pushNamed(context, '/chat', arguments: message.data['chatId']);
  }
});

Additionally, you can use FirebaseMessaging.instance.getInitialMessage() to handle the notification that caused the app to open from a terminated state.

Best Practices for Handling Push Notifications

  • User Consent: Always request user consent before sending push notifications.
  • Notification Channels: Use notification channels to categorize notifications and allow users to customize their notification preferences.
  • Payload Size: Keep the notification payload small to ensure reliable delivery.
  • Testing: Thoroughly test push notifications on both Android and iOS devices to ensure they are working as expected.
  • Error Handling: Implement proper error handling to manage situations where push notifications fail to deliver.
  • Security: Secure your FCM server key to prevent unauthorized sending of push notifications.

Conclusion

Handling different types of push notifications in Flutter involves understanding the nuances between notification and data messages, setting up Firebase Cloud Messaging, and implementing appropriate handlers for foreground and background scenarios. By leveraging libraries like flutter_local_notifications, you can create a comprehensive push notification strategy that enhances user engagement and provides timely information. By following best practices, you can ensure that your push notifications are effective, reliable, and respect user preferences.