I was assigned the task of handling system notifications for medication reminders. The notifications will include two actions: Take and Skip. The notification should function in all app states, including foreground, background, and terminated. Let's begin with these basic requirements and add more as we progress.
This blog will serve as a QA form for the challenges I faced while implementing this functionality.
Tools helped me along the way:
-
Mockoon - Used as a logging server instead of using
print
in the console. - Google Dev Playground - Used for obtaining an auth token to use Google APIs for sending notifications.
- Hoppscotch - HTTP client used for sending notifications through the Google API.
What libraries should use to achieve this functionality ?
firebase_messaging
, flutter_local_notifications
and permission_handler
.
How to listen to remote notification in all app states ?
1- Ensure that notification permission is configured and granted
// in AndroidManifist.xml add this permession
// <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
Permission.notification.request();
2- Initialize Firebase in the main
function.
await Firebase.initializeApp();
3- Configure notification callbacks.
FirebaseMessaging.onBackgroundMessage(_onBackgroundMessage);
FirebaseMessaging.onMessage.listen(_onForegroundMessage);
4- Implement notification callbacks.
@pragma('vm:entry-point')
Future<void> _onBackgroundMessage(RemoteMessage message) async {}
void _onForegroundMessage(RemoteMessage message) {}
-
_onBackgroundMessage
: Triggered when the app receives a remote notification in the background and terminated. -
_onForegroundMessage
: Triggered when the app receives a remote notification in the foreground.
Don't forget @pragma('vm:entry-point')
for the background callback to work properly. Now we can show notifications with actions in all states.
Hint: Sometimes, FirebaseMessaging
callbacks don't work properly without calling FirebaseMessaging.instance.getToken()
first.
How to add actions to remote notifications ?
We can't add actions to notification directly using firebase_messaging
package only, we need to use flutter_local_notifications
to implement this functionally
1- Initialize and configure flutter_local_notifications
.
// call this function in main
Future<void> configLocalNotification() async {
await FlutterLocalNotificationsPlugin().initialize(
const InitializationSettings(
android: AndroidInitializationSettings('@mipmap/ic_launcher'),
),
onDidReceiveNotificationResponse: _onForegroundNotificationResponse,
onDidReceiveBackgroundNotificationResponse: _onBackgroundNotificationResponse,
);
}
2- Implement action callbacks.
void _onForegroundNotificationResponse(NotificationResponse details) {}
@pragma('vm:entry-point')
void _onBackgroundNotificationResponse(NotificationResponse details) {}
-
_onForegroundNotificationResponse
: Triggered when the app receives a notification action in the foreground. -
_onBackgroundNotificationResponse
: Triggered when the app receives a notification action in the background and terminated. Don't forget@pragma('vm:entry-point')
for background callback to work properly. Now we can show notification with actions in all states.
3- Receive silent remote notifications or data-only notifications from the backend and then show local notifications with actions once the device receives remote notifications.
{
"message": {
"token": "",
"android": {
"priority": "HIGH"
},
"data": {
"title": "string",
"body": "string"
}
}
}
4- Show local notifications with actions on _onBackgroundMessage
and _onForegroundMessage
callbacks using the FlutterLocalNotificationsPlugin.show(...)
method.
How to redirect the user to a specific page if the app was opened from a notification tap?
Call FlutterLocalNotificationsPlugin().getNotificationAppLaunchDetails()
in the main
function to know if the app was opened from a notification or not.
How to bring app to foreground when app receives remote notification on background or terminated state ?
Using android_intent_plus
and package_info_plus
will help us achieve this feature:
1- Make sure that SYSTEM_ALERT_WINDOW
permission is granted
/// <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
// Used to show full app notifcation on lock screen
Permission.systemAlertWindow.request();
2- use android Intent
to open or bring app to foreground
Future<void> _bringAppToForeground() async {
final package = await PackageInfo.fromPlatform();
final packageName = package.packageName;
final intent = AndroidIntent(
action: 'android.intent.action.MAIN',
flags: [Flag.FLAG_ACTIVITY_NEW_TASK],
category: 'android.intent.category.LAUNCHER',
arguments: {'args': 'run flutter app automatically from notification'},
package: packageName,
componentName: '$packageName.MainActivity',
);
return intent.launch();
}
- we need to check
arguments
on app start to check if app was opened automatically from background - Also if we need to bring app to foreground when screen in locked we need to add
USE_FULL_SCREEN_INTENT
permission to manifest, and config main activity
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
<!-- add this keys to activity -->
<activity
...
android:showWhenLocked="true"
android:turnScreenOn="true">
</activity>
- To check if app was launched using the android
Intent
we need to check launcharguments
usinglaunch_args
orreceive_intent
package or simply implement native android function and call it using method channel and call it inmain
function
private fun getIntentArgs(): Map<String,Any?>?{
val args = intent.extras
val map = mutableMapOf<String,Any?>();
args?.keySet()?.forEach{
map[it] = args[it]
}
return if(args == null) null else map
}
const _methodChannel = MethodChannel('channel_name');
Future<Map<String, dynamic>?> getIntentArgs() async {
final argsResult = await _methodChannel.invokeMapMethod('getIntentArgs');
final args = argsResult?.cast<String, dynamic>();
return args;
}
How to close app once user responds to notification if app was opened from notification ?
Using the flutter_exit_app
package to close the app completely whenever we want.
How to Display App-Specific UI when Receiving Notifications in Foreground or Background?
While reacting to notification events in the foreground has no challenges since all listeners are registered in the main isolate, the complexity arises when dealing with background events. To address this, a communication mechanism between the main isolate and other isolates becomes essential.
The IsolateNameServer
API comes to our rescue in establishing straightforward isolate communication. We achieve this by registering the UIIsolateCommunicationChannel
in the main thread using the forceRegister
method. Additionally, the listen
function facilitates the reception of data through this channel, making use of the send
method to communicate with the UIIsolateCommunicationChannel
.
abstract class UIIsolateCommunicationChannel {
static final _receivePort = ReceivePort();
static const name = 'ui_isolate';
static bool register() => IsolateNameServer.registerPortWithName(
_receivePort.sendPort,
name,
);
static void forceRegister() {
final isRegistered = IsolateNameServer.registerPortWithName(
_receivePort.sendPort,
name,
);
if (isRegistered == false) {
IsolateNameServer.removePortNameMapping(name);
IsolateNameServer.registerPortWithName(
_receivePort.sendPort,
name,
);
}
}
static StreamSubscription listen(void Function(dynamic) listener) => _receivePort.listen(listener);
static void send(dynamic value) => IsolateNameServer.lookupPortByName(name)?.send(value);
static bool unregister() => IsolateNameServer.removePortNameMapping(name);
static bool isRegistered() => IsolateNameServer.lookupPortByName(name) != null;
}
Hints:
- Supported types for communication channel Link.
- Also, make sure to use primary constructors from
List
andMap
to pass them through communication channel to work on release mode . issue reference
UIIsolateCommunicationChannel.send((data.toList())) // fails in release mode ❌
UIIsolateCommunicationChannel.send((List.of(data))) // ✅
Not the End
I hope this small journey has been helpful for you.
Stay tuned for the IOS implementation
Resources:
- https://firebase.google.com/docs/cloud-messaging/concept-options
- https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages#Notification
- https://api.flutter.dev/flutter/dart-ui/IsolateNameServer-class.html
- https://medium.com/flutter/introducing-background-isolate-channels-7a299609cad8
- https://github.com/firebase/flutterfire/tree/master/packages/firebase_messaging/firebase_messaging/example
- https://github.com/MaikuB/flutter_local_notifications/tree/master/flutter_local_notifications/example
Top comments (0)