Featured Image
Software Development

Create Flutter Notifications using BLoc

In this article, I’ll be explaining broadly how I handle Notifications in Flutter using BLoc. There’s so much to cover that I’m going to dive right in!

 

Let’s start!

Before beginning, you must know how to setup firebase_messaging and local_notification in Flutter. There are plenty of tutorials that you can find online, and I will leave that task to your discretion.

 

Handling Notifications with BLoc

When you configure the firebase messaging plugin, messages are sent to your Flutter app via onMessageonLaunch, and onResume callbacks.

The following things are to be handled in NotificationBloc:

  1. Initialize local notification configurations to generate notifications when onMessage will be called
  2. Initialize firebase messaging plugin
  3. Firebase Cloud Messaging(FCM) token generation
  4. FCM token refresh
  5. Notification permission on the iOS platform
  6. Persist information when the app is launched from notifications

All of these cases must be handled within the main() of lib/main.dart file or in the MyApp widget itself because we need app-wide notifications.

void main() => runApp(MyApp());

But where should one initialize the required notification configurations, you ask? e.g., ask notification permission on iOS, FCM token generation.

We can do that before flutter starts rendering the app or MyApp widget renders the screen for the first time and refreshes the state of the widget if required.

The later will attach the widget to the tree asynchronously which can consist of network calls, image downloading, etc. Sounds a little unnecessary doesn’t it?

Here is a better way to do this:

If you want to do some awaiting tasks in main() then WidgetFlutterBinding.ensureInitialized() method must be executed before runApp(), like this:

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  ...
  ...
  ...
  runApp(MyApp());
}

In case you open the source code then you will find one of the comments saying,

You only need to call this method if you need the binding to be initialized before calling [runApp].

But what is WidgetFlutterBinding?

WidgetFlutterBinding is the glue between the widgets layer and the Flutter engine.

Enough of the theory, show me the code!

 

 


 

 

To start with Notification BLoc we need to define Events and States for the Bloc. Here is NotificationEvent.dart

  class NotificationEvent {
   
  //carries the payload sent for notification
  final String payload;
   
  const NotificationEvent(this.payload);
   
  }
   
  class NotificationErrorEvent extends NotificationEvent {
  final String error;
   
  const NotificationErrorEvent(this.error) : super(null);
  }
NotificationEvent.dart

NotificationEvent will carry notification payload and NotificationErrorEvent will carry error occurred during initialization or somewhere else.


 

 

We can represent the Notification state as follow.

  class NotificationState extends Equatable {
  const NotificationState();
   
  @override
  List<Object> get props => [];
  }
   
  class StartUpNotificationState extends NotificationState {}
   
  class IndexedNotification extends NotificationState {
  final int index;
   
  IndexedNotification(this.index);
   
  @override
  List<Object> get props => [this.index];
   
  @override
  bool operator ==(Object other) => false;
   
  @override
  int get hashCode => super.hashCode;
  }
NotificationState.dart

Here I’ve created base NotificationState class, StartUpNotificationState to represent the initial state of the BLoc and IndexedNotificationState to change the index of the Bottom Navigation page.

You should create your state class to handle every type of notifications for your app if required.


 

 

Now we should define NotificationBloc.

We will initialize the instances for local notification and firebase messaging instance in constructor.

NotificationBloc() {
 _localNotifications = new FlutterLocalNotificationsPlugin(); 
 _firebaseMessaging = new FirebaseMessaging(); 
}

We will require one initialize() method to initialize all the configurations in main() method.

initialize() async {
  NotificationAppLaunchDetails _appLaunchDetails =
      await _localNotifications.getNotificationAppLaunchDetails();

  var initializationSettings = _getPlatformSettings();
  await _localNotifications.initialize(initializationSettings,
      onSelectNotification: _handleNotificationTap);

  _createNotificationChannel();
  if (Platform.isIOS) {
    var hasPermission = await _requestIOSPermissions();
    if (hasPermission) {
      await _fcmInitialization();
    } else {
      add(NotificationErrorEvent(
          "You can provide permission by going into Settings later."));
    }
  } else {
    await _fcmInitialization();
  }

  _hasLaunched = _appLaunchDetails.didNotificationLaunchApp;
  if (_hasLaunched) {
    if (_appLaunchDetails.payload != null) {
      _payLoad = _appLaunchDetails.payload;
    }
  }
}

Following things are happening in the initialize() method,

  • Initializing local notification instance.
  • getNotificationLaunchDetails() is called which helps us with the information, whether the notification has triggered the app launch or not. This information will be used when our MyApp widget is attached to the root. (I’ll come to that part later in this article.)
  • Creating a notification channel for Android version ≥ 8 (Oreo).
  • Requesting notification permission for the iOS platform only.
  • Initializing FCM token if permission is provided on iOS and on Android it will be initialized directly without any checks.

Before we move forward with FCM initialization, we need to define one Notification class which can be used to represent one notification.

  part ‘notification.g.dart’;
   
  abstract class Notification
  implements Built<Notification, NotificationBuilder> {
  Notification._();
   
  factory Notification([updates(NotificationBuilder b)]) = _$Notification;
   
  @nullable
  String get notificationType;
   
  @nullable
  int get notificationId;
   
  @nullable
  String get notificationTitle;
   
  @nullable
  String get notificationBody;
   
  String toJson() {
  return json.encode(serializers.serializeWith(Notification.serializer, this));
  }
   
  static Notification fromJson(String jsonString) {
  return serializers.deserializeWith(
  Notification.serializer, json.decode(jsonString));
  }
   
  static Serializer<Notification> get serializer => _$notificationSerializer;
  }
Used built_value plugin to generate an immutable class.

Here I’m keeping notificationType member variable to identify the type of notification I need to handle. It will be used in mapEventToState() method to yield different states according to types. e.g Yield IndexedNotification to change the index of Bottom navigation.

Here is _fcmInitialization() method,

Future _fcmInitialization() async {
  try {
    _fcmToken = await _firebaseMessaging.getToken();

    _firebaseMessaging.onTokenRefresh.listen((event) {
      _fcmToken = event;
    });

    _firebaseMessaging.configure(
      onMessage: (Map<String, dynamic> message) async {
        Notification notification =
            convertToNotification(_notificationId++, message);
        await _showNotification(notification);
      },
      onLaunch: (Map<String, dynamic> message) async {
        print("onLaunch: $message");
        Notification notification =
            convertToNotification(_notificationId++, message);
        _hasLaunched = true;
        _payLoad = notification.toJson();
      },
      onResume: (Map<String, dynamic> message) async {
        print("onResume: $message");
        Notification notification =
            convertToNotification(_notificationId++, message);
        add(NotificationEvent(notification.toJson()));
      },
    );
  } catch (e) {
    add(NotificationErrorEvent(e.toString()));
  }
}

It does the following things.

  • Generates new FCM token
  • Attaching FCM token refresh handler to update the new token. Here I’m updating member variable with the token but we can call API to update new token in our back-end.
  • onMessage will convert received payload into the Notification object we defined earlier and create a new local notification. If the user taps on that notification it will call _handleNotificationTap().
  • onLaunch will assign _hasLaunched and _payload will be used when our MyApp widget is attached to the tree. I’ll come to that part later in this article soon.
  • onResume will add one NotificationEvent for Bloc to handle.
Future _handleNotificationTap(String payload) async {
  if (payload != null) {
    add(NotificationEvent(payload));
  }
}

This method is adding new events for Bloc to handle when the notification is tapped.

Now we will see how we can implement mapEventToState() method.

@override
Stream<NotificationState> mapEventToState(NotificationEvent event) async* {
  switch (event.runtimeType) {
    case NotificationEvent:
      Notification notification = Notification.fromJson(event.payload);
      if (notification.notificationType == Constants.notificationTypeIndex) {
        yield IndexedNotification(1);
      }
      break;
    case NotificationErrorEvent:
      yield NotificationErrorState((event as NotificationErrorEvent).error);
      break;
  }
}

I’ve handled one notification type here which will change the index of bottom navigation to 1. You can yield multiple states to handle different types of notifications.

Check out the complete code of NotificationBloc here.


 

 

Now, we will create an instance of NotificationBloc and call the initialize() method in the main() method. Like this,

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  NotificationBloc notificationBloc = new NotificationBloc();
  await notificationBloc.initialize();
  
  runApp(
    MultiBlocProvider(providers: [
      BlocProvider.value(value: notificationBloc),
    ], child: MyApp()),
  );
}

I’m providing the same notification bloc instance to MyApp() because we may want to navigate or change the state of the whole application when the notification is tapped. Right?


 

 

Here is my main.dart file.

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

NotificationBloc notificationBloc = new NotificationBloc();
  await notificationBloc.initialize();

runApp(
  MultiBlocProvider(providers: [
    BlocProvider.value(value: notificationBloc),
  ], child: MyApp()),
);

}

class MyApp extends StatefulWidget {
  const MyApp({Key key}) : super(key: key);

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

class _MyAppState extends State<MyApp> {

@override
void didChangeDependencies() {
  super.didChangeDependencies();
  BlocProvider.of<NotificationBloc>(context)
      .checkForLaunchedNotifications();
}

@override
Widget build(BuildContext context) {
  return BlocListener<NotificationBloc, NotificationState>(
    listener: (context, state) {
      if (state is IndexedNotification) {
        UiUtilities.showSnack(
            context, "Here you can navigate to index ${state.index}");
      } else if (state is NotificationErrorState) {
        UiUtilities.showSnack(context, state.error);
      }
    },
    child: MaterialApp(
      theme: bindTheme,
      onGenerateRoute: router.generateRoute,
      initialRoute: RouterConstants.myGuidRoute,
    ),
  );
}
}
 

Here I’m listening to state changes for NotificationBloc as I don’t want to rebuild the whole widget when the state changes. I just wanted to navigate to another screen, wanted to add events to other blocs, etc.

Now if you see didChangeDepedencies() method we’re identifying launch information and change the state accordingly. This is critical to handle because we don’t get a chance to handle app launch in main().

Pheww!! That’s how we can create NotificationBloc. Thanks for bearing with me.

Also, take a moment to explore this blog post on Flutter InitialRoute: Understanding the Role of the Slash “/”

author
Harsh Soni
Harsh is a developer with about a decade (if not more) of expertise in building mobile apps. More of a doer, less of a talker, he likes to keep his mind occupied, and has an eye for understanding systems in their barebones, helping him be his creative best, impacting users' lives for the better. Enjoys building reusable & scalable systems, reading a book in a quiet beautiful place, or learning something new.