Nelson Michael Nelson Michael is a frontend developer from Nigeria. When he's not meddling with CSS, he spends his time writing, sharing what he knows, and playing games.

Implementing local notifications in Flutter

6 min read 1833

Implementing Local Notifications In Flutter

Notifications alert users to important information about the apps and services they are subscribed to. They are aimed at improving user experience and driving engagement within an application.

When it comes to mobile apps, there are two types of notifications, push notifications and local notifications. In this article, we’ll implement local notifications into both Android and iOS platforms using the flutter
_local_notifications
package.

What are local notifications?

Using local notifications is a way to engage with your users and draw their attention back to your application without the use of an internet connection, and apps like Reminder and to-do apps make heavy use of them. They are generally pre-scheduled and fire when certain actions are performed by the user in the app.

Local notifications vs. push notifications

The major difference between local and push notifications is that local notifications are scheduled by an app locally and are delivered by the same device, whereas push notifications are sent from a remote server. Let’s build out a project so you can see how local notifications work.

Adding dependencies to the Flutter app

The first step is to run the command below in your terminal to add the latest version of flutter_local_notifications to your pubspec.yaml file.

//run this command in the terminal 
$ flutter pub add flutter_local_notifications

Next, create a new Dart file named notification_service.dart. You can give it any file name you want, but I prefer to name files according to their functionality.

In Flutter, it’s best practice to seclude your logic from your UI. To do this, we’ll create a class called NotificationService in the notification_service.dart file. This class will handle all the notification logic and expose methods to create, send, schedule, and cancel notifications.

import 'package:flutter_local_notifications/flutter_local_notifications.dart';

class NotificationService {
  //Singleton pattern
  static final NotificationService _notificationService =
      NotificationService._internal();
  factory NotificationService() {
    return _notificationService;
  }
  NotificationService._internal();

    //instance of FlutterLocalNotificationsPlugin
  final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = 
      FlutterLocalNotificationsPlugin();
}

Let’s analyze what’s happening in the code above:

We used the singleton pattern to create the NotificationService class. In addition, on line 12, we created an instance of FlutterLocalNotificationsPlugin, which initializes Flutter local notification settings for the Android and iOS platforms.

Configuring platform-specific initialization settings

Let’s walk through how we can configure initialization settings for both Android and iOS platforms.

We made a custom demo for .
No really. Click here to check it out.

Configuring Android initialization settings
To configure the Android initialization settings, we need to pass in a single required argument, which is the app icon that would be displayed in the notification bar:

final AndroidInitializationSettings initializationSettingsAndroid = 
  AndroidInitializationSettings('app_icon');

Now we need to add our icon as a drawable resource to the Android head project. Here’s the full path for doing this:

YOUR_APPLICATION_FOLDER_NAME\android\app\src\main\res\drawable\YOUR_APP_ICON.png

Configuring initialization settings for iOS
Configuring these settings for iOS is a little more complicated because we have to consider the multiple ways in which notifications are handled across different versions of the iOS operating system.

First, add the following lines to the didFinishLaunchingWithOptions method in the AppDelegate.swift file of your iOS project.

if #available(iOS 10.0, *) {
  UNUserNotificationCenter.current().delegate = self as? UNUserNotificationCenterDelegate
}

Our AppDelegate.swift file should look like this:

import UIKit
import Flutter
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    if #available(iOS 10.0, *) {
      UNUserNotificationCenter.current().delegate = self as UNUserNotificationCenterDelegate
    }
    GeneratedPluginRegistrant.register(with: self)
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
}

The IOSInitializationSettings object takes in three arguments: requestSoundPermission, requestBadgePermission, and requestAlertPermission. These arguments control which permission is being requested from the user.

Depending on your use case, you can choose to set all notification permissions to false, then call the requestIOSPermissions method with desired permissions at the appropriate point in your application, as shown below.

//Initialization Settings for iOS devices 
    final IOSInitializationSettings initializationSettingsIOS =
        IOSInitializationSettings(
      requestSoundPermission: false,
      requestBadgePermission: false,
      requestAlertPermission: false,
    );


 void requestIOSPermissions(
    FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin) {
  flutterLocalNotificationsPlugin
      .resolvePlatformSpecificImplementation<
          IOSFlutterLocalNotificationsPlugin>()
      ?.requestPermissions(
        alert: true,
        badge: true,
        sound: true,
      );
}

Creating the InitializationSettings object

The next step is to create an InitializationSettings object. This plugin is used to initialize settings for both Android and iOS platforms.

Generally, the InitializationSettings has three named optional parameters, android, iOS, and macOS, and they take in the corresponding platform initialization settings arguments.

final InitializationSettings initializationSettings =
        InitializationSettings(
            android: initializationSettingsAndroid,
            iOS: initializationSettingsIOS);

Following the configuration of our platform-specific initialization settings, we would create the method init, which would contain all of our initialization settings logic and be called from our main.dart file upon app launch.

 Future<void> init() async {

    //Initialization Settings for Android
    final AndroidInitializationSettings initializationSettingsAndroid =
        AndroidInitializationSettings('app_icon');

    //Initialization Settings for iOS 
    final IOSInitializationSettings initializationSettingsIOS =
        IOSInitializationSettings(
      requestSoundPermission: false,
      requestBadgePermission: false,
      requestAlertPermission: false,
    );

    //InitializationSettings for initializing settings for both platforms (Android & iOS)
    final InitializationSettings initializationSettings =
        InitializationSettings(
            android: initializationSettingsAndroid,
            iOS: initializationSettingsIOS);

    await flutterLocalNotificationsPlugin.initialize(
      initializationSettings,
    );
  }

In the code above, we pass in our platform-specific initialization settings into the InitializationSettings object.

The next step is to call the initialize method on the FlutterLocalNotificationsPlugin object. This method takes in two arguments, the InitializationSettings object and the onSelectNotification property.

The onSelectNotification property takes in a callback function that will be triggered when the notification is tapped. This function holds a single required argument called the payload, which holds any data that is passed through the notification.

Future selectNotification(String payload) async {
    await Navigator.push(
      context,
      MaterialPageRoute<void>(builder: (context) => SecondScreen(payload)),
    );
}

Here, this callback function will trigger navigation to SecondScreen and display the payload associated with the notification when the user taps on the notification.

Our init method should look like this now:

 Future<void> init() async {

    //Initialization Settings for Android
    final AndroidInitializationSettings initializationSettingsAndroid =
        AndroidInitializationSettings('app_icon');

    //Initialization Settings for iOS 
    final IOSInitializationSettings initializationSettingsIOS =
        IOSInitializationSettings(
      requestSoundPermission: false,
      requestBadgePermission: false,
      requestAlertPermission: false,
    );

    //InitializationSettings for initializing settings for both platforms (Android & iOS)
    final InitializationSettings initializationSettings =
        InitializationSettings(
            android: initializationSettingsAndroid,
            iOS: initializationSettingsIOS);

    await flutterLocalNotificationsPlugin.initialize(
      initializationSettings, 
      onSelectNotification: selectNotification
    );
  }

Future selectNotification(String payload) async {
    await Navigator.push(
      context,
      MaterialPageRoute<void>(builder: (context) => SecondScreen(payload)),
    );
}

Let’s return to our main.dart file. In the main function, we will call the init method and the requestiOSPermissions method to request permissions from the user as soon as the app starts on iOS devices.

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await NotificationService().init(); // 
  await NotificationService().requestIOSPermissions(); // 
  runApp(MyApp());
}

Displaying a notification in Flutter

To display a notification, we need to create a platform-specific NotificationDetails instance, which takes in arguments that are unique to each platform.

AndroidNotificationDetails handles the configuration of notifications in Android devices. It takes in several arguments, like channelID, channelName, channelDescription, priority, importance, and so on.

The iOSNotificationDetails handles the configuration of notifications in iOS devices and it takes in arguments such as presentAlert, presentBadge, badgeNumber, subtitle, sound, etc.

Here’s what the AndroidNotificationDetails instance looks like:

  AndroidNotificationDetails _androidNotificationDetails =
      AndroidNotificationDetails(
    'channel ID',
    'channel name',
    'channel description',
    playSound: true,
    priority: Priority.high,
    importance: Importance.high,
  );

Here’s what the iOSNotificationDetails instance looks like:

 IOSNotificationDetails _iosNotificationDetails = IOSNotificationDetails(
    presentAlert: bool?,
    presentBadge: bool?,
    presentSound: bool?,
    badgeNumber: int?
    attachments: List<IOSNotificationAttachment>?
    subtitle: String?, 
        threadIdentifier: String?
  );

Now the next step is to create a NotificationDetails object that takes in our platform-specific notification details objects as arguments.

const NotificationDetails platformChannelSpecifics = 
  NotificationDetails(
    android: _androidNotificationDetails,
    iOS: _iOSNotificationDetails);

Next we need to call the show method of the FlutterLocalNotificationsPlugin. The show method is responsible for creating push notifications, and it expects some arguments like id, title, body, notificationDetails, and payload.

id: unique identifier of a notification
title: title for the notification
body: the notification message
notificationDetails: where we pass in the notificationDetails object
payload: holds the data that is passed through the notification when the notification is tapped

await flutterLocalNotificationsPlugin.show(
      0,
      'Notification Title',
      'This is the Notification Body',
      platformChannelSpecifics,
      payload: 'Notification Payload',
    );

Now, let’s create a showNotification method and wrap all of this logic in it, then we can call this method from anywhere to display a notification.

class NotificationService {
  ....
  Future<void> showNotifications() async {
    await flutterLocalNotificationsPlugin.show(
      0,
      'Notification Title',
      'This is the Notification Body',
      platformChannelSpecifics,
      payload: 'Notification Payload',
    );
  }
}

Scheduling a local notification in Flutter

To schedule a notification, we need to call the zoneSchedule method of the FlutterLocalNotificationsPlugin. This method expects an instance of TZDateTime class, which is provided by the timezone package.

Because the flutter_local_notifications plugin already depends on the timezone package, there’s no need to add the timezone package as a dependency in our pubspec.yaml file. We only have to import it into our notification_service.dart file and initialize it.

import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:timezone/data/latest.dart' as tz;
import 'package:timezone/timezone.dart' as tz;

....

Future<void> init() async {
    final AndroidInitializationSettings initializationSettingsAndroid =
        AndroidInitializationSettings('app_icon');

    final IOSInitializationSettings initializationSettingsIOS =
        IOSInitializationSettings(
      requestSoundPermission: false,
      requestBadgePermission: false,
      requestAlertPermission: false,
    );

    final InitializationSettings initializationSettings =
        InitializationSettings(
            android: initializationSettingsAndroid,
            iOS: initializationSettingsIOS);

    //initialize timezone package here 
    tz.initializeTimeZones();  //  <----

    await flutterLocalNotificationsPlugin.initialize(
      initializationSettings, 
      onSelectNotification: selectNotification
    );
}

The zoneSchedule method takes in several arguments, including id, title, body, scheduledDate, notificationDetails, payload, uiLocalNotificationDateInterpretation, and androidAllowWhileIdle.

The scheduleDate parameter specifies when a notification should be displayed. androidAllowWhileIdle, when set to be true, ensures scheduled notifications are displayed regardless of if the device is in low-power mode.

await flutterLocalNotificationsPlugin.zonedSchedule(
        0,
        "Notification Title",
        "This is the Notification Body!",
        tz.TZDateTime.now(tz.local).add(const Duration(seconds: 5)),
        platformChannelSpecifics,
        androidAllowWhileIdle: true,
        uiLocalNotificationDateInterpretation:
            UILocalNotificationDateInterpretation.absoluteTime);

Now, let’s create a scheduleNotification method and wrap all of this logic in it, then we can call this method from anywhere to create a scheduled notification.

class NotificationService {
  ....
  Future<void> scheduleNotifications() async {
    await flutterLocalNotificationsPlugin.zonedSchedule(
        0,
        "Notification Title",
        "This is the Notification Body!",
        tz.TZDateTime.now(tz.local).add(const Duration(minutes: 5)),
        platformChannelSpecifics,
        androidAllowWhileIdle: true,
        uiLocalNotificationDateInterpretation:
            UILocalNotificationDateInterpretation.absoluteTime);
  }
}

Canceling a local notification in Flutter

When canceling a notification, you can either cancel a specific notification or cancel all pending notifications. Let’s walk through how you can do that:

Canceling a single notification
To cancel a specific notification, let’s create a new method called cancelNotification, which will contain the cancel method from the FlutterLocalNotificationsPlugin object. This method expects an argument, which is the id of the notification.

class NotificationService {
  ....
  Future<void> cancelNotifications() async {
    await flutterLocalNotificationsPlugin.cancel(NOTIFICATION_ID);
  }
}

Canceling all notifications
To cancel all pending notifications, let’s create a new method cancelAllNotifications, which will contain the cancelAll method from the FlutterLocalNotificationsPlugin object.

Unlike cancelling a single notification where its method takes in a single argument, this method doesn’t take in any argument.

class NotificationService {
  ....
  Future<void> cancelAllNotifications() async {
    await flutterLocalNotificationsPlugin.cancelAll();
  }
}

Here’s a GitHub repository that contains all of the code from this tutorial. If you’d like to see the final build, simply clone that repository and run it on your computer.

Conclusion

Local notifications are incredibly useful for notifying or alerting users about important information, and they can be implemented without an internet connection.

You can read the flutter_local_notifications package documentation to learn other ways you can use local notification in your projects.

: Full visibility into your web apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.

.
Nelson Michael Nelson Michael is a frontend developer from Nigeria. When he's not meddling with CSS, he spends his time writing, sharing what he knows, and playing games.

Leave a Reply