Flutter local notification explained for ios and android

Created At: 2021-09-12 05:53:39 Updated At: 2023-07-06 23:46:50

Flutter local notification with scheduling and notification with local storage sqflite. This app shows notification in certain intervals. It's more like a task management app, where you create a task and that could be notified in certain times later by notification. This text based tutorial upgrade the flutter video tutorial which supports 3.7.0 flutter version.

Part 1

Part 2

Part 3

1. iOS Settings

2. Work on initialization

3. For older version of iOS

4. Request Permisions for iOS

5. Scheduled Notification

6. Immediate Notification

7. Android Settings

8. BehaviorSubject

9. Configure Local Time Zone

10. Navigate to different route

11. Passing payload

12. Upgrade the app

12. Errors and Solutions

 

Get the complete code on buyme coffee. The code in this link is flutter 3.4 version.

Task management 

BLoC version of the app

Riverpod version of the app

 

The big picture

 

1. iOS Settings

In your project folder go to your ios folder and find AppDelegate.Swift

and add a code like above the picture. Add the code below

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

With the above code you are done with basic iOS settings.

 

2. Work on Initialization

First you need to instantiate the plugin like below 

  FlutterLocalNotificationsPlugin
  flutterLocalNotificationsPlugin =
      FlutterLocalNotificationsPlugin(); 

then we will use flutterLocalNotificationsPlugin for IOSInitializationSettings, AndroidInitializationSettings and passing a callback function to onSelectNotification.

First take a look at an important callback function for iOS below

onDidReceiveLocalNotification

 

3. For older version of iOS

  Future onDidReceiveLocalNotification(
      int id, String title, String body, String payload) async {
    // display a dialog with the notification details, tap ok to go to another page
    showDialog(
      //context: context,
      builder: (BuildContext context) => CupertinoAlertDialog(
        title: Text(title),
        content: Text(body),
        actions: [
          CupertinoDialogAction(
            isDefaultAction: true,
            child: Text('Ok'),
            onPressed: () async {
              Navigator.of(context, rootNavigator: true).pop();
              await Navigator.push(
                context,
                MaterialPageRoute(
                  builder: (context) => SecondScreen(payload),
                ),
              );
            },
          )
        ],
      ),
    );
  }

Must be called from initillization functions  ios. But this is only necessary if your ios version older than 10. 

For iOS you can call it from DarwinInitializationSettings() 
Use it like below

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

The above code is the latest iOS initialization settings which relies on a constructor name DarwinInitializationSettings

And then for android add the below code

 final AndroidInitializationSettings initializationSettingsAndroid =
     Android InitializationSettings("appicon);

The above code with make sure it will run on Android version. For android version we must mention the app icon name. The app icon name could be anything you like. But there must be an image for that name.

 

And then call IOSInitializationSettings from InitializationSettings like below

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

 

and then call the "initialize" function from  flutterLocalNotificationsPlugin like below

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

Do remember that, selectNotification function is a callback function which means it will get called later. Once again callback functions are functions that get called later. When somethigns happens later.  In our case selectNotification will be called like say 5 seconds or 5 minutes later.

Now let's see the complate initialization function  of flutterLocalNotificationsPlugin

  FlutterLocalNotificationsPlugin
  flutterLocalNotificationsPlugin =
      FlutterLocalNotificationsPlugin(); //

  initializeNotification() async {
    //tz.initializeTimeZones();
   // this is for latest iOS settings
    final DarwinInitializationSettings initializationSettingsIOS =
    DarwinInitializationSettings(
        requestSoundPermission: false,
        requestBadgePermission: false,
        requestAlertPermission: false,
        onDidReceiveLocalNotification: onDidReceiveLocalNotification
    );

 final Android InitializationSettings initializationSettingsAndroid =
     Android InitializationSettings("appicon);

    final InitializationSettings initializationSettings =
        InitializationSettings(
       iOS: initializationSettingsIOS,
       android:initializationSettingsAndroid,
    );
    await flutterLocalNotificationsPlugin.initialize(
        initializationSettings,
        onSelectNotification: selectNotification);

  }

 

Before we start to use the notification we must get permission from users in iOS. Use the code below to get permission

4. Request Permisions for iOS

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

Call the requestIOSPermissions() inside your initialize method.

 

Now take a look at selectNotification callback fuction for onSelectNotification. We need to implement it inside initialization settings. With this function we would be able to go to another page once the notification arrives by tapping on the notification tab. 

We implement it like below

For on tap onSelectNotification

  Future selectNotification(String payload) async {
    if (payload != null) {
      print('notification payload: $payload');
    } else {
      print("Notification Done");
    }
     Get.to(()=>SecondScreen(payload));
  }

As you can see it takes us to new page. Awesome. Here we use GetX package for navigation. With GetX you don't need to pass the context. It becomes much easier. 

This callback function also takes the payload (the infomation you want to send after on tap to a new route or page).

We are done with settings. Now we can create another function for showing notification on the screen when the time arraive. 

iOS Latest Settings

♠ Sqflite step by step ♠

5. Scheduled Notification

   scheduledNotification() async {
     await flutterLocalNotificationsPlugin.zonedSchedule(
         0,
         'scheduled title',
         'theme changes 5 seconds ago',
         tz.TZDateTime.now(tz.local).add(const Duration(seconds: 5)),
         const NotificationDetails(
             android: AndroidNotificationDetails('your channel id',
                 'your channel name', 'your channel description')),
         androidAllowWhileIdle: true,
         uiLocalNotificationDateInterpretation:
             UILocalNotificationDateInterpretation.absoluteTime);

   }

The above function is the most basic scheduled notification which implements flutterLocalNotificationsPlugin.zonedSchedule

Here we get a notification after 5 seconds once the app has been initialized. You need to call scheduledNotification() from somewhere in your app.

The line about time  TZDateTime is quiet important.

tz.TZDateTime.now(tz.local).add(const Duration(days:0, minutes: 0, seconds: 5)),

You can only pass constants to it's duration parameters. So we passed 5(a constant) seconds to it. If you try to send a variable you will get error like

Error: Not a constant expression.

This also means that, if you want to pass a time from somewhere else, it won't work. You will get the above error.

Of course, you can change the Duration function and send dynamic time.

Here you can add a property to make it periodically or in fixed schedule notification.

matchDateTimeComponents: DateTimeComponents.time

This property makes sure that, the notification would pop up periodically or in a fixed time manner or daily.

Now instead of .time you can use .DayOfWeekAndTime. This would show the notification every week on cerntain time and day.

matchDateTimeComponents: DateTimeComponents.DayOfWeekAndTime

 

6. Immediate Notification 

Instant or immediate notification could be match together on when you tap on a button. Just call the below function from the onTap() event on your button.

  displayNotification({required String title, required String body}) async {
    print("doing test");
    var androidPlatformChannelSpecifics = new AndroidNotificationDetails(
        'your channel id', 'your channel name', 'your channel description',
        importance: Importance.max, priority: Priority.high);
    var iOSPlatformChannelSpecifics = new IOSNotificationDetails();
    var platformChannelSpecifics = new NotificationDetails(
        android: androidPlatformChannelSpecifics, iOS: iOSPlatformChannelSpecifics);
    await flutterLocalNotificationsPlugin.show(
      0,
      'You change your theme',
      'You changed your theme back !',
      platformChannelSpecifics,
      payload: 'It could be anything you pass',
    );
  }

The above code will also repsone and navigate to a different page, when you tap on the notification.

Call this function from your initialize function. Take a look at the initialize function now

 initializeNotification() async {
 final IOSInitializationSettings initializationSettingsIOS =
     IOSInitializationSettings(
         requestSoundPermission: false,
         requestBadgePermission: false,
         requestAlertPermission: false,
         onDidReceiveLocalNotification: onDidReceiveLocalNotification
     );

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

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

 

7. Android Settings

You need to find the AndroidManifest.xml file and make the necessary changes. You need to provide the below settings

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.flutterappnoti">

    <!-- The INTERNET permission is required for development. Specifically,
         flutter needs it to communicate with the running application
         to allow setting breakpoints, to provide hot reload, etc.
    -->
    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.VIBRATE" />
    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>

 

And then app icon for android. Without setting app icons you will get crash

Set it inside the drawable folder

Warnings for Android users

You should also make sure that, you installed java and jdk on your machines to run android emulator. If your java and jdk environment paths are not defined, you won't be able to run android emulator.

If you get error saying too many positional argument 2 expectd but 3 found  for onDisplayNotification on Android try to remove "your channel description" this field for Android

8. BehaviorSubject

BehaviorSubject is a controller(Stream), you can add items to it and emit items from it to a listener. So all it does is, wait for events(like notifications) to happen and capture the events and forward them to a listener.

BehaviourSubject is important if you want to click on the the notification and go to another page or to a route with payload.

Once you click on the notificatoin tab, BehaviorSubject will capture the lastest(this) notification and it's related info like payload string (the info you want to send with the notifications).

You can create an instance like below

  final BehaviorSubject<String> selectNotificationSubject =
  BehaviorSubject<String>();

Now selectNotificationSubject will have some properties that you can use like

selectNotificationSubject.add(payload);
selectNotificationSubject.stream.listen(()=>);

So selectNotificationSubject.add should be called inside onSelectNotification callback. We know that, onSelectNotification gets called duration the initialization of notifications.

 onSelectNotification: (String payload) async {
          if (payload != null) {
            debugPrint('notification payload: ' + payload);
          }
          selectNotificationSubject.add(payload);
});

Now with this, payload would be added for the next route. And listener will receive this event for executing later (based on your notification time).

And later BehaviourSubject selectNotificationSubject instance would send the added payload to the listener.

So if you want to tap on notification message and go to another page, you must implement BehaviorSubject. And the process is first add the items to this on onSelectNotififcation callback and then call for listening from the first initialization.

9. Configure Local Time Zone

You need to configure local time zone if you want to trigger a notification on certain time later. For this you must access the local time zone from native layer.

flutter_native_timezone: ^1.0.10

You need to install this package and you need to import the three packages

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

Then in your initialization you put like below

tz.initializeTimeZones();
final String timeZoneName = await FlutterNativeTimezone.getLocalTimezone();
tz.setLocalLocation(tz.getLocation(timeZoneName));

The first line does the necessary settings for the time zone and second line get the time zone from the native layer of the device and third line sets the time.

10. Navigate to different route

Now you will see how to navigate to different routes. You can use selectNotification call back to do that. In our case the body is selectNotification. We can do a conditional check to navigate to different routes

   Future selectNotification(String payload) async {
    if (payload != null) {
      //selectedNotificationPayload = "The best";
      //selectNotificationSubject.add(payload);
      print('notification payload: $payload');
    } else {
      print("Notification Done");
    }

   if(payload=="Theme Changed"){
      //going nowhere
      }else{
        Get.to(()=>SecondScreen(payload));
      }
  }

11. Pass payload

Payload is always gotten from the functions you call for notification. In our case, we are calling displayNotification and scheduledNotification from our front end fluter code.

They must carry and pass payload from flutter caller to callee. See the callee

  scheduledNotification(int hour, int minutes,Task task) async {
    await flutterLocalNotificationsPlugin.zonedSchedule(
      task.id,task.title,task.note,
      ...........................................................
      ..........................................................
      matchDateTimeComponents: DateTimeComponents.time,
      payload: "${task.title}|"+"${task.note}|"+"${task.startTime}|",
    );
}

And the other one

  displayNotification({@required String title, @required String body}) async {
    ....................................................................
   ....................................................................
    await flutterLocalNotificationsPlugin.show(
      0,
      'You change your theme',
      'You changed your theme back !',
      platformChannelSpecifics,

      payload: title,
    );
  }

So you see how we pass the paylaod. 

Our callback function onSelectNotification also gets the same payload. So you can get them in this callback function and send anywhere you want to navigate for.

   Future selectNotification(String payload) async {
    if (payload != null) {
      print('notification payload: $payload');
    } else {
      print("No plaload");
    }

      if(payload=="Theme Changed"){
      }else{
       Get.to(()=>SecondScreen(payload));
      }
  }

This is how you can pass payload to a different page or route

 

12. Upgrading the app

If you need to upgrade the app, wanna run your app on flutter stable version. Go ahead and upgrade the app using the below commands

 

And then make sure that, you have the following versions.

With the above plugin vesions you will have the latest version of flutter on your side. We also removed flutter_icons plugin.

And the make sure you do the following changes in your app. 

12.1 Additional changes

In task_controller.dart make the following changes

  final RxList<Task> taskList = List<Task>.empty().obs;

And

  Future<void> addTask({required Task task}) async {
     await DBHelper.insert(task);
  }

And your notifications_services.dart onDidReceiveLocalNotification() function, use context:Get.context!

  Future onDidReceiveLocalNotification(
      int id, String? title, String? body, String? payload) async {
    // display a dialog with the notification details, tap ok to go to another page
    showDialog(
      context: Get.context!,
      builder: (BuildContext context) => CupertinoAlertDialog(
        title: Text(title!),
        content: Text(body!),
        actions: [
          CupertinoDialogAction(
            isDefaultAction: true,
            child: Text('Ok'),
            onPressed: () async {
              Navigator.of(context, rootNavigator: true).pop();
              await Navigator.push(
                context,
                MaterialPageRoute(
                  builder: (context) => SecondScreen(payload),
                ),
              );
            },
          )
        ],
      ),
    );
  }

 

And in the same file you should remove the necessary arguments. In android studio, the unnecessary arguments are underlined and remove them.

And through out the if you are using icons from flutter_icons, remove those icons and use any custom icons. You should be good to go

 

12. Errors and Solutions

1.  LocationNotFoundException (Location with the name "GMT" doesn't exist)

Solution 1

Are you working on an emulator ? By default, there is no timezone set. So you have to choose one so the GMT will not be null.

Have you tried to change the timezone in phone's settings ?

I was working on emulator and changing timezone from United States to France stopped crashing my app

Solution 2

import 'package:timezone/data/latest_all.dart' as tz;
instead of
import 'package:timezone/data/latest.dart' as tz;

Make sure you are importing the right way and right package

Solution 3

Try not to use Apple Sillicon Android Emulator

static Future configureLocalTimeZone() async {
tz.initializeTimeZones();
final String timeZoneName = await FlutterNativeTimezone.getLocalTimezone();
Logger.log('Timezone: $timeZoneName', className: '$AppConfig');
try {
tz.setLocalLocation(tz.getLocation(timeZoneName));
} catch () {
// Failed to get timezone or device is GMT or UTC, assign generic timezone
const String fallback = 'Africa/Accra';
Logger.log('Could not get a legit timezone, setting as $fallback',
className: '$AppConfig');
tz.setLocalLocation(tz.getLocation(fallback));
}
}

You can also try the above code for the solution

Issues

With new version of flutter, you may need to change the primaryColor like below

'primaryColor' doesn't work. Instead use 'colorSchemeSeed'. This works for both light and dark mode. primarySwatch is also a alternative, but it only works for light mode, not dark.

Error and Solutions

Int is not subtype of String

In the above error case the line 

 title = json['title'];
 title = json['title'].toString();

Other fixes

Comment

Add Reviews