Flutter: Using the local_notifications as provider

Hello friends, it's my third month with flutter and today we are gping to look on how to use the local_notification package. Reading the documentation is a little bit a roller coster for me, so i decided to spend my weekend writing this just in case someone need a reference to improve upon. Howover Spending time reading the documentation and repo really helps and i encourage you to do that on any libfary that you use.

Instead of covering the feature, caveats etc that you can easily find by reading the docs, I'm gonna focus on the implementation as of my other post as this blog is dedicated for code reference rather than being though provoking or advocates.

But if you asked me what framework to write a cross platform mobile apps, i would say try flutter (i've worked with android java before, swift, ionic, react native and xamarin). Flutter is what i vibe with. Unless you need a very platform specific, it's either flutter or swift (swift is very good to develop ios app imo). But that was just my opinion, you know what best for you.

So let's get back on topic of implementing the local notifications package as provider so that it can be accessible from anywhere (within the app).

Btw, if you have question, need some help or feedback, don't hesitate to reach me on twitter.

Getting Started

  1. Include flutter_local_notifications: ^<LATEST_VERSION> in pubspec.yaml

  2. Execute flutter clean

  3. Execute flutter pub get

Setting Up for Android

Go to <PROJECT_FOLDER>/android/app/src/main/AndroidManifest.xml and compare with yours and add the line as in the code blocks as necessary, don't copy all of it.

  • the RECEIVE_BOOT_COMPLETED permission is make sure our scheduled notifications works after reboots

  • if you need vibration control (which most of the time the default is enough) add this permission as well

    xml <uses-permission android:name="android.permission.VIBRATE" />

  • the ScheduledNotificationReceiver is to allow the package to handle the schedule notifications

  • the ScheduledNotificationBootReceiver blocks is to make sure schedule notifications are still there upon reboot and after application updates

<!-- Flutter local_notifications -->
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>

<!-- Before this -->
<application >
  ...
  </activity> <!-- After this -->

  <!-- Flutter local_notifications -->
  <receiver android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationReceiver" />
  <receiver android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationBootReceiver">
      <intent-filter>
          <action android:name="android.intent.action.BOOT_COMPLETED"/>
          <action android:name="android.intent.action.MY_PACKAGE_REPLACED"/>
      </intent-filter>
  </receiver>

  <!-- Don't delete the meta-data below.
        This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
  <meta-data
      android:name="flutterEmbedding"
      android:value="2" />
</application>

Add proguard rules **important

  • create files android/app/proguard-rules.pro and should contain the following if not exist.

  • without this file, flutter release won't crash but your apk and appbundle will crash whenever you try to use notification
  • GSON are used by local_nofication thus the proguard rules need to be added as well
# local_notification 
-keep class com.dexterous.** { *; }

# gson
-keepattributes Signature
-keepattributes *Annotation*
-dontwarn sun.misc.**
-keep class com.google.gson.examples.android.model.** { <fields>; }
-keep class * extends com.google.gson.TypeAdapter
-keep class * implements com.google.gson.TypeAdapterFactory
-keep class * implements com.google.gson.JsonSerializer
-keep class * implements com.google.gson.JsonDeserializer
-keepclassmembers,allowobfuscation class * {
  @com.google.gson.annotations.SerializedName <fields>;
}

Android Notification Channel

Setting up On iOS

  • navigate to <PROJECT_FOLDER>/ios/Runner/AppDelegate.swift and add this two blocks into the application function block

// remove alarm on reinstallation
if(!UserDefaults.standard.bool(forKey: "Notification")) {
  UIApplication.shared.cancelAllLocalNotifications()
  UserDefaults.standard.set(true, forKey: "Notification")
}

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

Full code

import UIKit
import Flutter

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    GeneratedPluginRegistrant.register(with: self)

    // remove alarm on reinstallation
    if(!UserDefaults.standard.bool(forKey: "Notification")) {
      UIApplication.shared.cancelAllLocalNotifications()
      UserDefaults.standard.set(true, forKey: "Notification")
    }

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

    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
}

Android notification icon

  • copy the smallest icon from mipmap and past it inside drawable folder

  • named as app_icon.png or any name which we will refer to in the code later

Local Notification Provider

Please do customize it to suit your needs

import 'package:flutter/foundation.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';

// Test are only possible during integration test on a real device

class ReceivedNotification {
  final int id;
  final String title;
  final String body;
  final String payload;

  ReceivedNotification(
      {@required this.id,
      @required this.title,
      @required this.body,
      @required this.payload});
}

class LocalNotification with ChangeNotifier {
  FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
      FlutterLocalNotificationsPlugin();
  AndroidInitializationSettings androidInitializationSettings;
  IOSInitializationSettings iosInitializationSettings;
  InitializationSettings initializationSettings;
  int pendingNotificationCount = 0;

  LocalNotification() {
    initLocalNotification();
  }

  int get totalNotifcation => pendingNotificationCount;

  Future<void> updateNotificationCount() async {
    List<PendingNotificationRequest> pendingNotifcationRequests =
        await flutterLocalNotificationsPlugin?.pendingNotificationRequests();
  }

  Future<int> getHighestNotificationId() async {
    List<PendingNotificationRequest> pendingNotifcationRequests =
        await flutterLocalNotificationsPlugin?.pendingNotificationRequests();
    int maxId = 0;
    pendingNotifcationRequests?.forEach((item) { 
      if (item.id > maxId) maxId = item.id;
    });
    return maxId;
  }

  Future<int> getNotificationId(String title) async {
    List<PendingNotificationRequest> pendingNotifcationRequests =
        await flutterLocalNotificationsPlugin?.pendingNotificationRequests();
    final notification =
        pendingNotifcationRequests?.firstWhere((item) => item.title == title, orElse: () => null,);
    if (notification != null) return notification.id;
    return null;
  }

  Future<void> cancelAllNotifications() async {
    await flutterLocalNotificationsPlugin?.cancelAll();
    await updateNotificationCount();
    notifyListeners();
  }

  Future<void> cancelNotificationById(int notificationId) async {
    await flutterLocalNotificationsPlugin.cancel(notificationId);
  }

  Future<dynamic> onDidReceiveLocalNotification(
      int id, String title, String body, String payload) async {
    return ReceivedNotification(
        id: id, title: title, body: body, payload: payload);
  }

  Future<dynamic> onSelectNotification(String payload) async {
    if (payload != null) {
      debugPrint('notification payload: ' + payload);
    }
    // do something with notification based on paylod
  }

  Future<void> initLocalNotification() async {
    androidInitializationSettings = AndroidInitializationSettings('app_icon');
    iosInitializationSettings = IOSInitializationSettings(
        onDidReceiveLocalNotification: onDidReceiveLocalNotification);
    initializationSettings = InitializationSettings(
        androidInitializationSettings, iosInitializationSettings);
    await flutterLocalNotificationsPlugin?.initialize(initializationSettings,
        onSelectNotification: onSelectNotification);
    await updateNotificationCount();
  }

  Future<void> showNotification(
      {@required String channelID,
      @required String channelName,
      @required String channelDesc,
      @required String notificationTitle,
      @required String notificationBody,
      String payload}) async {

    AndroidNotificationDetails androidNotificationDetails =
        AndroidNotificationDetails(
      channelID,
      channelName,
      channelDesc,
      priority: Priority.High,
      importance: Importance.Max,
      ticker: channelName,
    );
    IOSNotificationDetails iosNotificationDetails = IOSNotificationDetails();
    NotificationDetails notificationDetails =
        NotificationDetails(androidNotificationDetails, iosNotificationDetails);

    await flutterLocalNotificationsPlugin?.show(0,
        notificationTitle, notificationBody, notificationDetails,
        payload: 'item X');
  }

  Future<void> scheduledNotification(
      {@required String channelID,
      @required String channelName,
      @required String channelDesc,
      @required String notificationTitle,
      int notificationId,
      @required String notificationBody,
      @required DateTime notificationTime}) async {

    DateTime scheduledNotificationDateTime = notificationTime;

    AndroidNotificationDetails androidNotificationDetails =
        AndroidNotificationDetails(
      channelID,
      channelName,
      channelDesc,
      priority: Priority.High,
      importance: Importance.Max,
      ticker: '$channelName',
    );

    IOSNotificationDetails iosNotificationDetails = IOSNotificationDetails();
    NotificationDetails notificationDetails =
        NotificationDetails(androidNotificationDetails, iosNotificationDetails);

    await flutterLocalNotificationsPlugin?.schedule(
        0,
        notificationTitle,
        notificationBody,
        scheduledNotificationDateTime,
        notificationDetails,
        payload: 'item X');
  }
}

Init the Provider

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import './provider/data_provider.dart';
import './app_root.dart';
import './helper/db_helper.dart';
import './provider/local_notification.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    SystemChrome.setPreferredOrientations(
      [DeviceOrientation.portraitUp]);
    return MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (_) => DBHelper()),
        ChangeNotifierProvider(create: (_) => LocalNotification()),
        ChangeNotifierProxyProvider<DBHelper, DataProvider>(
          create: (context) => DataProvider(null, []),
          update: (context, db, previous) =>  DataProvider(db, previous.allItems),
        )
      ],
      child: MaterialApp(
        ...
      ),
    );
  }
}

Using the provider function

This is a working example of using the local notification provider. I'm using pretty similar setup in my project.

  • the rule is as other provider, set 'listen: false' when we call the function, or flutter will spit out an error telling you to do so anyway since we are not subscribing to any data, we just calling the function

Notification ID (int)

  • notifcaion id is important and should be different for each notifications, or else it will replaced the existing noitification with the same id.

  • in my usage, i get the highest notification id and then +1 for every new notification and store it inside the provider each time the app start.

  • to delete or cancel notification you have to use the id (int), on my usage i use the title for the item as the notification title, so unless you have the same item with the same title, it is safe to look for id from the pending list. Otherwise you have to figure out to store you id inside a storage with your data or check the id does not exist in the list yet before scheduled it.

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../provider/local_notification.dart';

class MorePage extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    final totalNotification = Provider.of<LocalNotification>(context).totalNotifcation;
    return SafeArea(
        child: Container(
      decoration: BoxDecoration(
          color: Colors.white,
          border: Border(top: BorderSide(color: Colors.grey[300], width: 1))),
      height: double.infinity,
      child: SingleChildScrollView(
        child: Column(
          children: <Widget>[
            ListTile(
                leading: const Icon(Icons.notifications_active),
                title: const Text('Test Notification'),
                subtitle: const Text('Test Notification'),
                onTap: () => Provider.of<LocalNotification>(context, listen: false)
                    .scheduledNotification(
                      channelID: 'Channel ID',
                      channelName: 'Channel Name',
                      channelDesc: 'Channel Description',
                      notificationId: 1,
                      notificationTitle: 'Date Tracker Test',
                      notificationBody: 'We are showing notification!',
                      // change to any time you want
                      notificationTime: DateTime.now().add(Duration(seconds: 10))
                    )
            ),
            const Divider(),
            ListTile(
                leading: const Icon(Icons.notifications_off),
                title: const Text('Clear All Notification'),
                subtitle: const Text('Clear All Notification'),
                onTap: () => Provider.of<LocalNotification>(context, listen: false)
                    .cancelAllNotifications()
            ),
            const Divider(),
            ListTile(
                leading: const Icon(Icons.notifications),
                title: const Text('Clear All Notification'),
                subtitle: const Text('Clear All Notification'),
                trailing: Text('$totalNotification'),
            ),
            const Divider(),
          ],
        ),
      ),
    ));
  }
}

References