Flutter — Firebase push notification 2023 with redirection from notification (FCM)

Apoorv Pandey
7 min readJun 30, 2023

--

Introduction to FCM:

Firebase Cloud Messaging is a cloud based messaging solution (notification) that allows the developers or web servers to send notifications to the users using the App. It can be automated obviously for instance when user place an order in the app and if the user has left the app and the order gets confirmed the user will receive a notification saying “Your order has been confirmed” or “Your order has been dispatched”.

Setting up the Firebase Console:

Go to Firebase Console and sign in with your desired Google Account, and follow the below steps:

  1. Click on Add Project.
  2. Enter the name and follow the on screen steps.
  3. As I am writing this there is a new Flutter App option available now.
  4. Select Flutter and use the new Firebase CLI.

Add Firebase to your Flutter Project:

To add dependency use the commands:

flutter pub add firebase_core
flutter pub add firebase_messaging

Run these both commands one by one or as you like this will automatically add these dependencies into your pubspec.yaml file.

Open main.dart file and add this code:

import 'package:firebase_core/firebase_core.dart';

Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
runApp(const MyApp());
}

Create a new file called messaging_service and paste the below all code in that file.

import 'dart:developer';

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

class MessagingService {
static String? fcmToken; // Variable to store the FCM token

static final MessagingService _instance = MessagingService._internal();

factory MessagingService() => _instance;

MessagingService._internal();

final FirebaseMessaging _fcm = FirebaseMessaging.instance;

Future<void> init(BuildContext context) async {
// Requesting permission for notifications
NotificationSettings settings = await _fcm.requestPermission(
alert: true,
announcement: false,
badge: true,
carPlay: false,
criticalAlert: false,
provisional: false,
sound: true,
);

debugPrint(
'User granted notifications permission: ${settings.authorizationStatus}');

// Retrieving the FCM token
fcmToken = await _fcm.getToken();
log('fcmToken: $fcmToken');

// Handling background messages using the specified handler
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);

// Listening for incoming messages while the app is in the foreground
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
debugPrint('Got a message whilst in the foreground!');
debugPrint('Message data: ${message.notification!.title.toString()}');

if (message.notification != null) {
if (message.notification!.title != null &&
message.notification!.body != null) {
final notificationData = message.data;
final screen = notificationData['screen'];

// Showing an alert dialog when a notification is received (Foreground state)
showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return WillPopScope(
onWillPop: () async => false,
child: AlertDialog(
title: Text(message.notification!.title!),
content: Text(message.notification!.body!),
actions: [
if (notificationData.containsKey('screen'))
TextButton(
onPressed: () {
Navigator.pop(context);
Navigator.of(context).pushNamed(screen);
},
child: const Text('Open Screen'),
),
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Dismiss'),
),
],
),
);
},
);
}
}
});

// Handling the initial message received when the app is launched from dead (killed state)
// When the app is killed and a new notification arrives when user clicks on it
// It gets the data to which screen to open
FirebaseMessaging.instance.getInitialMessage().then((message) {
if (message != null) {
_handleNotificationClick(context, message);
}
});

// Handling a notification click event when the app is in the background
FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
debugPrint(
'onMessageOpenedApp: ${message.notification!.title.toString()}');
_handleNotificationClick(context, message);
});
}

// Handling a notification click event by navigating to the specified screen
void _handleNotificationClick(BuildContext context, RemoteMessage message) {
final notificationData = message.data;

if (notificationData.containsKey('screen')) {
final screen = notificationData['screen'];
Navigator.of(context).pushNamed(screen);
}
}
}

// Handler for background messages
@pragma('vm:entry-point')
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
// If you're going to use other Firebase services in the background, such as Firestore,
// make sure you call `initializeApp` before using other Firebase services.
debugPrint('Handling a background message: ${message.notification!.title}');
}

1. `init(BuildContext context)`:
— This function is responsible for initializing the messaging service and setting up the necessary configurations.
— It takes a `BuildContext` parameter to provide the context of the app.
— Inside this function, the permission for receiving notifications is requested from the user using the `requestPermission` method of Firebase Messaging.
— The FCM token is retrieved using the `getToken` method and stored in the `fcmToken` variable.
— The background message handler is set using the `onBackgroundMessage` method.
— The `onMessage` listener is set to handle incoming messages while the app is in the foreground. When a message is received, an alert dialog is shown to the user with the message details.
— The `getInitialMessage` method is used to handle the initial message received when the app is launched from a terminated state. If there is a message, the `_handleNotificationClick` function is called.
— The `onMessageOpenedApp` listener is set to handle a notification click event when the app is already open. If a notification is clicked, the `_handleNotificationClick` function is called.

2. `_handleNotificationClick(BuildContext context, RemoteMessage message)`:
— This function is responsible for handling a notification click event by navigating to the specified screen.
— It takes a `BuildContext` parameter to provide the context of the app and a `RemoteMessage` parameter that represents the received message.
— The function retrieves the notification data from the message and checks if it contains a key named ‘screen’.
— If the ‘screen’ key is present, the function retrieves the screen value and navigates to that screen using `Navigator.of(context).pushNamed(screen)`.

3. `_firebaseMessagingBackgroundHandler(RemoteMessage message)`:
— This function serves as the handler for background messages.
— It takes a `RemoteMessage` parameter representing the received message in the background.
— The function is marked with `@pragma(‘vm:entry-point’)` to indicate that it is the entry point for handling background messages in the Dart VM.
— In this example, the function simply logs the title of the background message notification.

initialize the MessagingService class in your home page not main.dart if you do then you’ll get error while navigating to other screens from notification.

import 'package:flutter/material.dart';
import 'package:push_notification/messaging_service/messaging_service.dart';

class HomePage extends StatefulWidget {
const HomePage({super.key});

@override
State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
final _messagingService =
MessagingService(); // Instance of MessagingService for handling notifications

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Home')),
body: const Center(child: Text('Welcome')),
);
}

@override
void initState() {
super.initState();
_messagingService
.init(context); // Initialize MessagingService to handle notifications
}
}

If you get error like below while running:

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':app:mapDebugSourceSetPaths'.
> Error while evaluating property 'extraGeneratedResDir' of task ':app:mapDebugSourceSetPaths'
> Failed to calculate the value of task ':app:mapDebugSourceSetPaths' property 'extraGeneratedResDir'.
> Querying the mapped value of provider(java.util.Set) before task ':app:processDebugGoogleServices' has completed is not supported

Upgrade these dependencies to latest version you’ll see the yellow highlited line:

from:
classpath 'com.android.tools.build:gradle:7.3.0'
classpath 'com.google.gms:google-services:4.3.10'
to:
classpath 'com.android.tools.build:gradle:7.3.1'
classpath 'com.google.gms:google-services:4.3.15'

As I am writing this the latest version is as above.

Till the steps we have done, we should be receiving notifications in our app.

Go to Firebase Console and go to Engage tab in the left menu. Click on Messaging then click on Create Your First Campaign then click on Firebase Notification messages.

Enter the notification title and notification text, then click on Send test message.

Copy the FCM Token from the console (App):

Click on + Icon and click on test.

After hitting test we have received the notification in the app.

Foreground:

Background:

Background:

Kill the app and run it again manually by clicking on the app icon from the launcher (your phone). Because When running from Android Studio the Android Studio Terminates the app. Means the app will do nothing even in background.

The notification should work.

For Redirecting user to another screen from notification:

Go to Cloud Messaging and click here
Enable Cloud Messaging
Go back to console and refresh and you’ll see the server key

Open Postman and follow the steps:

  1. Create a POST request
  2. Enter the URL https://fcm.googleapis.com/fcm/send
  3. Add the Header Authorization Bearer <server key>
  4. Add Body and add raw JSON as below
  5. In the JSON “to” key add the FCM Token
Header for FCM Request
Body
{
"notification": {
"title": "You have a new notification",
"body": "This is a body text"
},
"data": {},
"to": "PASTE THE FCM TOKEN HERE"
}

You shall receive the notification as soon as you hit the SEND button in POSTMAN

Follow the steps below for redirecting to other screen from notification click

  1. Create a new page e.g. NotificationsPage
  2. In the main.dart file define routes as below
class MyApp extends StatelessWidget {
const MyApp({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(
routes: {'/notifications': (context) => const NotificationsPage()},
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const HomePage(),
);
}
}

3. Run the app

4. Add the below JSON to POSTMAN close and re open your app then put in the background and test user will be redirected to NotificationsPage also from dead state.

{
"notification": {
"title": "You have a new notification",
"body": "This is a body text"
},
"data": {
"screen": "/notifications"
},
"to": "PASTE THE FCM TOKEN HERE"
}

NOTE: To get the FCM token to send in API Use the code:

MessagingService.fcmToken!

Video tutorial: https://youtu.be/RiAE8tv1sG8

Any problems/suggestion reach me out or comment.

Thanks!

GitHub Repo here

--

--

Apoorv Pandey

👨‍💻 Passionate Software Engineer diving into the digital realm and beyond! 💻✨