DEV Community

Kleekit
Kleekit

Posted on

Push Notification Using Firebase, Node.js, Flutter/Dart

If you are a junior developer or this is the first time exploring this concept, you must have asked the question “how can two or more devices communicate remotely in real time?”. If you are a very smart person, you might have thought of some solutions yourself. Your solutions might revolve around the following:

Use a stream: If you are familiar with the concept of streaming in programming, you might think this will be a solution to your problem. All you need to do is store the data in the database and listen to the presence of such data at the recipient’s device.
Use a web-socket: web sockets are employed in most real time communications such as text, audio and video messaging channels. So, maybe this can be a very reliable option for you.

You may even come up with some crude technique that may involve doing a periodic query of an API within the app to expose any new data. While all of these are valid and industry standard methods of sharing data in real time, it depends on their individual use case within the application.

If data can be shared across devices in the above mentioned manner, what then is push notification? Why is it a critical feature in modern applications? How can push notifications be implemented in an application? This article will provide very clear answers to these questions.

What is Push Notification?

Most mobile applications run on either the Android or the iOS operating system. Each of the devices that run on these operating systems have a unique identifier. This identifier is commonly referred to as device token. Each operating system has a service which can remotely communicate with each device running on the operating system. On iOS, the service is the Apple Push Notification service (APNs) while on Android it is the Firebase Cloud Messaging (FCM) service.

Push notifications are enabled in an application when the application sends data (which will also include the device token of the targeted device) to the push notification service (APNs or FCM). The push notification service will in turn send the data to the specific device to trigger the device to display the notification irrespective of whether the application is actively running on the device or not.

Image description

Note that push notifications can be implemented in other operating systems apart from Android and iOS, but in the context of this article, we will only focus on the use of push notification on Android and iOS.

Why Use Push Notification in Modern Applications?

Push notification has become a very critical feature for most mobile and web applications. Why is this so? Here are some reasons:

Message Alert: Just like the name implies, push notification notifies or alerts a user of an application of an incoming or already received data.
Marketing: Push notification is used as a marketing tool to inform users of an application of a service offering or some other promotional messages.
User retention: Some users tend to forget about an app they rarely use. Strategic alerts from these apps help the user get back on the app.
Engagement: Social media applications and other “engagement seeking” applications might utilize the power of push notification to help improve the engagement on the application and getting the user invested into the application.

One of the fascinating things about push notifications is that the targeted user may or may not be a registered user of the application. As long as the application runs on the user’s device and the user has enabled push notification from that app, information can be shared to that device. In addition, an app does not need to be actively opened (or in use) to receive a push notification. This is one of the most powerful reasons for push notification. The various methods of sharing data remotely as highlighted previously would require an active usage of the application for it to be successful. For example, I would not know of having received a chat message from Whatsapp if i do not have whatsapp opened. But with push notification, I can be notified of having received a Whatsapp message (even when the phone is idle), before I proceed to opening Whatsapp to see the contents of the message or engage further with the sender. Thus, push notification can be used in conjunction with other data sharing methods in an application.

How to Implement Push Notification in an Application

Push notification can be programmatically implemented in an application. The application would need to ping the push notification service to allow the push notification service to ping the targeted device. We are going to look at implementation of push notification both at the backend and at the frontend. Push notification can be implemented only at the frontend without any backend support, but this is not the best practice.

Prerequisite

Before we can fully get started with the programmatically implementing push notification, you need a firebase account setup. If you do not have a firebase account, you can create one here. Your firebase account is the same as your google account. So if you have a google account, you are good to go.

Now that you have a google account, you can proceed to the firebase console. On the firebase console, you will need to create a new project or use an existing project (if you already have one).

Image description

Click on Add Project and fill the form to create a new project. Ensure to enter all details accurately. After each form, click on continue until the end where you create project.

After your project is created, you can download the service account json file. To do this, go to your firebase console, on your console click your_project_name > project overview settings(icon) > project settings > service accounts > generate new private key > generate key

Note that you are to download this key and keep it in a secured place. You cannot retrieve this key, if it is misplaced, you will need to generate another key. There is no major implication to generating multiple keys, your app would still work well.

Backend Implementation of Push Notification

We will consider the implementation of push notification at the backend using node.js. If you are familiar with JavaScript, this would be easy to follow through. We would be needing one major package added to this project:

  • Firebase admin: Firebase admin is a firebase core package that helps us handle a lot of firebase services. One of the firebase services we need is the firebase messaging service. Note that we previously highlighted that Firebase Cloud messaging is the push notification service for Android devices, and you might want to ask: “What if I want to send push notification to an iOS device, do I need another package?” — No you do not! The firebase messaging package would help us with both Android and iOS push notification. It will ping the android and iOS push notification services and deliver the notification to the appropriate device. When firebase admin is installed, please configure it in your project. The configuration could look like this:
// in your main.js or app.js file
const admin = require('firebase-admin');
// download your service account credential from firebase
// and add it to a config folder in your project
const serviceAccount = require('../config/serviceAccountKey.json');

admin.initializeApp({
  credential: admin.credential.cert(serviceAccount),
  databaseURL: 'https://yourFirebaseProjectName.firebaseio.com',
});
Enter fullscreen mode Exit fullscreen mode

By now, I want to assume that you have set up your node.js project and you are ready to integrate push notification. I would not go into the details of other project setup, I would only be focused on the push notification function. Now let’s get properly started with the following steps:

  • Write a send notification function: This function would be asynchronous and would require parameters such as user id, image url (for notification with an image), title, body, resource id. The resource id is the database id of the information you want to send. For example if you are sending a message notification, the resource id would be the message id. The resource id is important because you may need to do some frontend query when the notification is opened. This query can be done using the resource id or some other parameter you use in querying data in your own backend. The body is the longer text to be shown to the user when the notification is expanded. Note that all of these parameters are optional parameters apart from the ‘title’.
//this is your service.js file or some file where you want to handle push notification
const admin = require('firebase-admin');

async sendPushNotification(title, body, userId, imageUrl, resourceId){}
Enter fullscreen mode Exit fullscreen mode
  • Query the user id to get more user data for the notification: We need additional information to be able to send push notification. The data we need is the device token and the badge count. The badge count is not a required parameter. It would only be vital for iOS devices. The badge is used to indicate the number of unread notifications for the user. The token is the device token. This can be gotten from the device itself and saved to the user collection on the database. When we discuss the frontend implementation of push notification, we will talk about how to get the device token. Note that for the purpose of this article the badge and device token is to be stored in the user collection. In some cases these data might need to be stored in its own collection with other information. The choice is completely up to you.
//this is your service.js file or some file where you want to handle push notification
const admin = require('firebase-admin');

async sendPushNotification(title, body, userId, imageUrl, resourceId){
  // the function below is a db call. replace with your respective db query
  const user = await findUserById(userId);

  const token = user.token;
  let badge = user.badge ?? 0; // if user.badge is null badge is assigned a 0 value

  if(token === null || token === undefined){
    throw new Error('this is an invalid token');
  }
  badge++; // increament the badge.
}
Enter fullscreen mode Exit fullscreen mode
  • Prepare the push notification message object: If you check the firebase messaging official docs, you will notice that the message object is of type TokenMessage | TopicMessage | ConditionMessage. and all 3 types are extended from a BaseMessage type. You can check the firebase official docs to see all the possible parameters of a firebase object. We will prepare a use case for our notification.
//this is your service.js file or some file where you want to handle push notification
const admin = require('firebase-admin');

async sendPushNotification(title, body, userId, imageUrl, resourceId){
  // the function below is a db call. replace with your respective db query
  const user = await findUserById(userId);

  const token = user.token;
  let badge = user.badge ?? 0; // if user.badge is null badge is assigned a 0 value

  if(token === null || token === undefined || token.length < 2){
    throw new Error('this is an invalid token');
  }
  badge++
  const message = {
          notification: {
            title,
            body,
          },
          android: {
            notification: {
              channel_id: 'MESSAGE_CHANNEL',// *
              icon: 'message_icon', // *
              tag: 'message', // *
              image: imageUrl,
            },
          },
          apns: {
            payload: {
              aps: {
                badge,
                sound: 'chime.caf',
              },
            },
          },
          data: {
            click_action: 'FLUTTER_NOTIFICATION_CLICK', // *
            type: 'MESSAGE', // *
            resourceId,
          },
          token,
       };
}
Enter fullscreen mode Exit fullscreen mode

Note: All the parameters in the code snippet above with “*” can be edited to fit your app. Please read more about each parameter in the firebase docs.

  • Send the push notification: Use the firebase admin package to send the push notification as shown below.
//this is your service.js file or some file where you want to handle push notification
const admin = require('firebase-admin');

async sendPushNotification(title, body, userId, imageUrl, resourceId){
  // the function below is a db call. replace with your respective db query
  const user = await findUserById(userId);

  const token = user.token;
  let badge = user.badge ?? 0; // if user.badge is null badge is assigned a 0 value

  if(token === null || token === undefined || token.length < 2){
    throw new Error('this is an invalid token');
  }
  badge++
  const message = {
          notification: {
            title,
            body,
          },
          android: {
            notification: {
              channel_id: 'MESSAGE_CHANNEL',// *
              icon: 'message_icon', // *
              tag: 'message', // *
              image: imageUrl,
            },
          },
          apns: {
            payload: {
              aps: {
                badge,
                sound: 'chime.caf',
              },
            },
          },
          data: {
            click_action: 'FLUTTER_NOTIFICATION_CLICK', // *
            type: 'MESSAGE', // *
            resourceId,
          },
          token,
       };

    await admin.messaging().send(message);
}
Enter fullscreen mode Exit fullscreen mode
  • Final clean up: Here you might want to update the badge count in your user collection and do some error handling.
//this is your service.js file or some file where you want to handle push notification
const admin = require('firebase-admin');

async sendPushNotification(title, body, userId, imageUrl, resourceId){
  // the function below is a db call. replace with your respective db query
  const user = await findUserById(userId);

  const token = user.token;
  let badge = user.badge ?? 0; // if user.badge is null badge is assigned a 0 value

  if(token === null || token === undefined || token.length < 2){
    throw new Error('this is an invalid token');
  }
  badge++
  const message = {
          notification: {
            title,
            body,
          },
          android: {
            notification: {
              channel_id: 'MESSAGE_CHANNEL',// *
              icon: 'message_icon', // *
              tag: 'message', // *
              image: imageUrl,
            },
          },
          apns: {
            payload: {
              aps: {
                badge,
                sound: 'chime.caf',
              },
            },
          },
          data: {
            click_action: 'FLUTTER_NOTIFICATION_CLICK', // *
            type: 'MESSAGE', // *
            resourceId,
          },
          token,
       };

    await admin.messaging().send(message);

  // update user badge count.
  // this is a db call. replace this method with your appropraite db update method
  await user.update({badge});
}
Enter fullscreen mode Exit fullscreen mode

It is important to use try-catch within the function body. This will help you to handle errors properly without breaking the server. One error that can be handled is when the device token is invalid. it might be necessary to remove the token from your db storage. This would help you to query the frontend later to collect the accurate device token.

  • Write another function to reset the badge count: After a notification is delivered, the user will click on the notification to view it. On the frontend we need to reset the badge count on the database so that we can have a zero value. This helps us manage viewed notifications. This badge count is mostly relevant for iOS apps.
async resetBadgeCount(userId){
  // this is a db call, replace with appropriate query to fetch user from db
  const user = await findUserById(userId);

  // this is a db update method. update the badge property of the user to 0
  await user.update({badge: 0});
}
Enter fullscreen mode Exit fullscreen mode

These push notification functions we have written for the backend can be called within other service functions across the application, or it can be exposed to an API. If they are exposed to an API, it means that push notifications can be triggered via an API call.

The resetBadgeCount function can be exposed to an API since it will be called on the frontend when the user clicks a notification. You can set up the API yourself.

Now that we have completed the backend implementation of the push notification feature, it is now time to focus on the frontend implementation in your app.

Frontend Implementation of Push Notification

Since our discussion of push notification has been focused on mobile apps, we would need to consider various frontend approaches for push notifications. Either we will be using a native language such as Java or Swift or a hybrid language such as Flutter and React native. In this article, we will only focus on Flutter. If you notice our backend implementation, you will see FLUTTER_NOTIFICATION_CLICK as one of the values of the push notification message object. This is because we are using Flutter for the front end. Please follow the firebase documentation on setting up your client. It is a very straightforward process.

So here are the following steps for front end implementation in Flutter:

  • Install Flutter Packages: You will need to install the following packages:

  • Firebase core: Before using any firebase you need to have firebase core package in your application.

  • Firebase Messaging: This is the main package we need for the push notification feature.

  • Flutter Local Notification: This package will help configure the front end to receive push notifications in the desired manner. It works in conjunction with the Firebase messaging package.

  • Flutter App Badger: This package will be used to reset the notification badge for iOS apps.

  • Add a new firebase app: On your firebase account, go to the project settings and add a new app. Select the iOS icon to add an iOS app

  • Download info.plist file: A GoogleService.info.plist file is automatically generated for you inside your project after creating the iOS app. Download this file. You will need it in your project.

  • Add info.plist file to project: Open the ios folder of the project directory in Xcode. Drag and drop the file you downloaded into the Runner subfolder. When a dialog box appears, make sure the Copy items if needed of the Destination property is checked and Runner is selected in the Add to targets box. Then, click Finish.

  • Download google services.json file: Download the google-services.json file, drag and drop it into your_project_directory > android > app then, click Next, Then follow the instructions on the page. Ensure to complete each instruction.

  • Create a Flutter Local Notification class and configure local notification: In one of your service files, you can create a class to setup and configure local notification.

// import all relevant packages here

class LocalNotificationService {
  static final localNotification = FlutterLocalNotificationsPlugin();

  static Future<void> initialize()async{
    // configure android to use the app icon for notification
    const AndroidInitializationSettings initializationSettingsAndroid =
        AndroidInitializationSettings('@mipmap/ic_launcher');

    // this function would be used for iOS apps when notification is received
    // it can be configured as needed.
    void onDidReceiveLocalNotification(
        int? id, String? title, String? body, String? payload) async {
      print('the notification arrived ');
    }

    // darwin settings is for iOS and MacOS
    final DarwinInitializationSettings initializationSettingsDarwin =
        DarwinInitializationSettings(
      requestSoundPermission: true,
      requestBadgePermission: true,
      requestAlertPermission: true,
      onDidReceiveLocalNotification: onDidReceiveLocalNotification,
    );

    // android and ios settings are thus embedded together
    final InitializationSettings settings = InitializationSettings(
      android: initializationSettingsAndroid,
      iOS: initializationSettingsDarwin,
    );

    // initialize the plugin with the configured settings.
    await localNotification.initialize(
      settings,
    );

  }
}
Enter fullscreen mode Exit fullscreen mode
  • Initialize the notification service: On the main.dart file, initialize the flutter notification service.
Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();

  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );

  // this is where the notification service was initialized
  await LocalNotificationService.initialize();

  runApp(MyApp());
}
Enter fullscreen mode Exit fullscreen mode
  • Create a firebase messaging class and configure firebase messaging: Firebase messaging will need to be initialized for the various notification events.
// import all necessary packages

// this function was written out of the class intentionally.
// if written in a class, it might cause a null check bug within the app.
Future<void> _backgroundMsgHandler(RemoteMessage? message) async {
  // you can specify how you want to handle bugs in your app when messages...
  // are received and the app is in background mode.
}

class FirebaseMessagingService {
  static final firebaseInstance = FirebaseMessaging.instance;

  static Future<void> init() async {
    /**
     * When the app is completely closed (not in the background)
     * and opened directly from the push notification
     */
    FirebaseMessaging.instance.getInitialMessage().then((RemoteMessage? event) {
      // specify how you want to handle the notification
    });

    //Background
    FirebaseMessaging.onBackgroundMessage(_backgroundMsgHandler);

    /**
     * When the app is open and it receives a push notification
     */
    FirebaseMessaging.onMessage.listen((RemoteMessage message) async {
      // specify how you want to handle the notification
    });

    /**
     * When the app is in the background and is opened directly
     * from the push notification.
     */
    FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
      // specify how you want to handle the notification
    });

  }

  // this function handles app permissions 
  Future<void> grantAppPermission() async {
    NotificationSettings settings = await firebaseInstance.requestPermission(
      alert: true,
      announcement: true,
      badge: true,
      provisional: false,
      sound: true,
    );

    // handle final permission settings from user as appropriate
    if (settings.authorizationStatus == AuthorizationStatus.authorized) {
      print('User granted permission');
    } else if (settings.authorizationStatus ==
        AuthorizationStatus.provisional) {
      print('User granted provisional permission');
    } else {
      print('User declined or has not accepted permission');
    }

  }

}
Enter fullscreen mode Exit fullscreen mode

For each message event, you might want to handle the messages differently. Some messages might need a redirection action, some might require a form of pop up dialog. Whatever your app preference, you can achieve it within your app.

  • Initialize the firebase messaging on the home page: On the entry point of your application, initialize firebase messaging.
class Home extends StatefulWidget {
  const Home({Key? key}) : super(key: key);

  @override
  _HomeState createState() => _HomeState();
}

class _HomeState extends State<Home> {
  Future<void> initFM() async {
    await FirebaseMessagingService.grantAppPermission();
    await FirebaseMessagingService.init();
  }

  @override
  void initState() {
    initFM();
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Container();
  }

}
Enter fullscreen mode Exit fullscreen mode
  • Update the local notification service: The Local Notification Service would need to be updated to add the function to reset badge count for iOS devices.
// import the flutter app badger package
// import http package

class LocalNotificationService {
  ..//all previously written codes 

  static Future<void> resetBadgeCountToZero(Store<AppState> store) async {
     // call the api to reset badge count on the db.
      http.get(Uri.parse('backend_url_to_reset_badge'));

    // this updates the UI badge for iOS
    FlutterAppBadger.removeBadge();
  }

}
Enter fullscreen mode Exit fullscreen mode

This resetBadgeCountToZero function can be called in the firebase init method for each of the notification events as needed.

Push notification is obviously a very interesting feature. The more you work with it, the more comfortable you are implementing it. Note that you can send push notifications directly from your firebase dashboard without interacting with your backend. This is mostly for development and testing purposes. It is also used during app operations to send broadcast notifications to multiple users. This article however focused on the use of a node.js backend because it meets the needs of most developers for their application.

Resources

The following resources could help you further in setting up push notifications for your app:

  1. Firebase core Package

  2. Firebase Messaging Package

  3. Firebase console

  4. Flutter App Badger Package

  5. Flutter Local Notification Package

  6. Further reading on a log rocket article

  7. Contact me on Twitter

Top comments (0)