Flutter: Push notifications even if the app is closed - push-notification

I have built an application with flutter that works like a reminder.
How can I display notifications to the user even though the app is closed?

For reminders i would recomend Flutter Local Notifications Plugin. It has a powerful scheduling api. From the documentation of local notification:
Scheduling when notifications should appear - Periodically show a
notification (interval-based) - Schedule a notification to be shown
daily at a specified time - Schedule a notification to be shown weekly
on a specified day and time - Ability to handle when a user has tapped on a notification when the app is the foreground, background or terminated
And for push notification, you can use Firebase Cloud Messaging
or one signal plugin or you can implement natively through platform-channels
Edit: You can also fire notifications according to specific conditions even if the app is terminated. This can be achevied by running dart code in the background. Quoting from the official faq:
Can I run Dart code in the background of an Flutter app? Yes, you can
run Dart code in a background process on both iOS and Android. For
more information, see the Medium article Executing Dart in the
Background with Flutter Plugins and Geofencing.

I have found a solution to this problem. We just have to register the Local Notification Plugin in the Application class.
First Create a class FlutterLocalNotificationPluginRegistrant, I have created this in Kotlin.
class FlutterLocalNotificationPluginRegistrant {
companion object {
fun registerWith(registry: PluginRegistry) {
if (alreadyRegisteredWith(registry)) {
Log.d("Local Plugin", "Already Registered");
return
}
FlutterLocalNotificationsPlugin.registerWith(registry.registrarFor("com.dexterous.flutterlocalnotifications.FlutterLocalNotificationsPlugin"))
Log.d("Local Plugin", "Registered");
}
private fun alreadyRegisteredWith(registry: PluginRegistry): Boolean {
val key = FlutterLocalNotificationPluginRegistrant::class.java.canonicalName
if (registry.hasPlugin(key)) {
return true
}
registry.registrarFor(key)
return false
}
}}
Now create a Application class extending FlutterApplication and implement PluginRegistry.PluginRegistrantCallback.
class Application : FlutterApplication(), PluginRegistry.PluginRegistrantCallback {
override fun onCreate() {
super.onCreate()
}
override fun registerWith(registry: PluginRegistry?) {
if (registry != null) {
FlutterLocalNotificationPluginRegistrant.registerWith(registry)
}
}}
and register the Application class in the AndroidManifest.xml
<application
android:name="com.packagename.Application"/>
All done. Now write a function to show notification and call it from the background handler method of Firebase messaging.
Future _showNotificationWithDefaultSound(String title, String message) async {
var androidPlatformChannelSpecifics = AndroidNotificationDetails(
'channel_id', 'channel_name', 'channel_description',
importance: Importance.Max, priority: Priority.High);
var iOSPlatformChannelSpecifics = IOSNotificationDetails();
var platformChannelSpecifics = NotificationDetails(
androidPlatformChannelSpecifics, iOSPlatformChannelSpecifics);
await flutterLocalNotificationsPlugin.show(
0,
'$title',
'$message',
platformChannelSpecifics,
payload: 'Default_Sound',
);
}
and call it like this.
Future<dynamic> myBackgroundMessageHandler(Map<String, dynamic> message) async {
if (message['data'] != null) {
final data = message['data'];
final title = data['title'];
final body = data['message'];
await _showNotificationWithDefaultSound(title, message);
}
return Future<void>.value();
}

I have also faced this issue, So these are my learnings
In my Case : i am able to get notification in App-Resume or App-background state, but in App-Close state, I am not receiving notifification.
In this case our notification body was :
{notification: {body: null, title: null}, data: {body: hello, title: world}}
To Receive Notification in App-Closed state we changed notification to
{notification: {body: abc, title: abc}, data: {url: string, body: string, title: string}}

You can use scheduled notifications in flutter.
var scheduledNotificationDateTime =
new DateTime.now().add(new Duration(seconds: 5));
var androidPlatformChannelSpecifics =
new AndroidNotificationDetails('your other channel id',
'your other channel name', 'your other channel description');
var iOSPlatformChannelSpecifics =
new IOSNotificationDetails();
NotificationDetails platformChannelSpecifics = new
NotificationDetails(
androidPlatformChannelSpecifics, iOSPlatformChannelSpecifics);
await flutterLocalNotificationsPlugin.schedule(
0,
'scheduled title',
'scheduled body',
scheduledNotificationDateTime,
platformChannelSpecifics);

For those who are using the latest version around 2.2 just call the firebaseMessageInstance
FirebaseMessaging.instance.getInitialMessage().then((message) =>
message.messageId.isNotEmpty
? print('we can now navigate to specific screen')
: print('there is no new notification so default screen will be shown when application start from terminated state'));
Don't forget to call the
Navigator.push(
context, MaterialPageRoute(builder: (context) => YourScreenName()));
when message.messageId.isNotEmpty
upvote if you like this approach thanks have a good coding day

If you do not need to connect to the Internet, you can use this packages flutter local notification && flutter native timezone
after add the package to pubspace.ymal
add this code to android/app/src/main/AndroidManifest.xml
<activity
android:showWhenLocked="true"
android:turnScreenOn="true">
also in ios folder open if you used swift Runner/AppDelegate.swift in function didFinishLaunchingWithOptions add
if #available(iOS 10.0, *) {UNUserNotificationCenter.current().delegate = self as? UNUserNotificationCenterDelegate}
if you used Object-C Runner/AppDelegate.m in function didFinishLaunchingWithOptions add
if (#available(iOS 10.0, *)) {[UNUserNotificationCenter currentNotificationCenter].delegate = (id<UNUserNotificationCenterDelegate>) self;
}
after that you should add app-icon to drawable folder
then import the packages import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:timezone/data/latest.dart' as tz; import 'package:timezone/timezone.dart' as tz; in file dart create and add
class NotifyHelper {
FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
FlutterLocalNotificationsPlugin();
String selectedNotificationPayload = '';
final BehaviorSubject<String> selectNotificationSubject =
BehaviorSubject<String>();
initializeNotification() async {
tz.initializeTimeZones();
_configureSelectNotificationSubject();
await _configureLocalTimeZone();
// await requestIOSPermissions(flutterLocalNotificationsPlugin);
final IOSInitializationSettings initializationSettingsIOS =
IOSInitializationSettings(
requestSoundPermission: false,
requestBadgePermission: false,
requestAlertPermission: false,
onDidReceiveLocalNotification: onDidReceiveLocalNotification,
);
const AndroidInitializationSettings initializationSettingsAndroid =
AndroidInitializationSettings('appicon');
final InitializationSettings initializationSettings =
InitializationSettings(
iOS: initializationSettingsIOS,
android: initializationSettingsAndroid,
);
await flutterLocalNotificationsPlugin.initialize(
initializationSettings,
onSelectNotification: (String? payload) async {
if (payload != null) {
debugPrint('notification payload: ' + payload);
}
selectNotificationSubject.add(payload!);
},
);
}
displayNotification({required String title, required String body}) async {
print('doing test');
var androidPlatformChannelSpecifics = const AndroidNotificationDetails(
'your channel id', 'your channel name', 'your channel description',
importance: Importance.max, priority: Priority.high);
var iOSPlatformChannelSpecifics = const IOSNotificationDetails();
var platformChannelSpecifics = NotificationDetails(
android: androidPlatformChannelSpecifics,
iOS: iOSPlatformChannelSpecifics);
await flutterLocalNotificationsPlugin.show(
0,
title,
body,
platformChannelSpecifics,
payload: 'Default_Sound',
);
}
// this is the scheduled notification
// Task is a model class have a data item like title, desc, start time and end time
scheduledNotification(int hour, int minutes, Task task) async {
await flutterLocalNotificationsPlugin.zonedSchedule(
task.id!,
task.title,
task.note,
//tz.TZDateTime.now(tz.local).add(const Duration(seconds: 5)),
_nextInstanceOfTenAM(hour, minutes),
const NotificationDetails(
android: AndroidNotificationDetails(
'your channel id', 'your channel name', 'your channel description'),
),
androidAllowWhileIdle: true,
uiLocalNotificationDateInterpretation:
UILocalNotificationDateInterpretation.absoluteTime,
matchDateTimeComponents: DateTimeComponents.time,
payload: '${task.title}|${task.note}|${task.startTime}|',
);
}
tz.TZDateTime _nextInstanceOfTenAM(int hour, int minutes) {
final tz.TZDateTime now = tz.TZDateTime.now(tz.local);
tz.TZDateTime scheduledDate =
tz.TZDateTime(tz.local, now.year, now.month, now.day, hour, minutes);
if (scheduledDate.isBefore(now)) {
scheduledDate = scheduledDate.add(const Duration(days: 1));
}
return scheduledDate;
}
void requestIOSPermissions() {
flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation<
IOSFlutterLocalNotificationsPlugin>()
?.requestPermissions(
alert: true,
badge: true,
sound: true,
);
}
Future<void> _configureLocalTimeZone() async {
tz.initializeTimeZones();
final String timeZoneName = await FlutterNativeTimezone.getLocalTimezone();
tz.setLocalLocation(tz.getLocation(timeZoneName));
}
/* Future selectNotification(String? payload) async {
if (payload != null) {
//selectedNotificationPayload = "The best";
selectNotificationSubject.add(payload);
print('notification payload: $payload');
} else {
print("Notification Done");
}
Get.to(() => SecondScreen(selectedNotificationPayload));
} */
//Older 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: const Text('Title'),
content: const Text('Body'),
actions: [
CupertinoDialogAction(
isDefaultAction: true,
child: const Text('Ok'),
onPressed: () async {
Navigator.of(context, rootNavigator: true).pop();
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => Container(color: Colors.white),
),
);
},
)
],
),
);
*/
Get.dialog( Text(body!));
}
//I used Get package Get here to go screen notification
void _configureSelectNotificationSubject() {
selectNotificationSubject.stream.listen((String payload) async {
debugPrint('My payload is ' + payload);
await Get.to(() => NotificationScreen(payload));
});
}
}
use object from this class and call the scheduledNotificationmethod

Related

Flutter push notification's with firebase

In my case, the notification only arrives on the device from which the new item in the list has been added, other users do not receive the notification. I would like other users to receive notifications. Currently I'm using the topic (the same case is with the device token).
Here is my code...
This is my function for post notification on firebase server:
Future postNotification() async {
final postUrl = 'https://fcm.googleapis.com/fcm/send';
final data = {
'to': '/topics/weather',
"collapse_key": "type_a",
"notification": {
"title": channel.name,
"body": channel.description,
},
};
final headers = {
'content-type': 'application/json',
'Authorization':
'key = myserverkey'
};
final response = await http.post(
Uri.parse(postUrl),
body: json.encode(data),
encoding: Encoding.getByName('utf-8'),
headers: headers,
);
print(response.statusCode);
}
For this global functions I get response status code 200 and everything look's fine, for topic name in this example I use weather.I call this function when adding a new element to the list.
This is my function for show notifications and I use this function in home screen of my app:
void showNotification() async {
flutterLocalNotificationsPlugin.show(
0,
channel.name,
channel.description,
NotificationDetails(
android: AndroidNotificationDetails(
channel.id,
channel.name,
channel.description,
importance: Importance.high,
color: Colors.blue,
playSound: true,
icon: '#mipmap/ic_launcher',
),
),
);
}
This is how I call the show Notification function on the home page of my application:
#override
void didChangeDependencies() {
super.didChangeDependencies();
FirebaseMessaging.onMessage.listen((RemoteMessage message) async {
RemoteNotification notification = message.notification;
AndroidNotification android = message.notification.android;
if (notification != null && android != null) {
final messageTitle = message.notification.body;
FirebaseMessaging.instance.subscribeToTopic('weather');
showNotification();
print(messageTitle);
}
});
}
Do I need an extra backend to make this work on other devices as well?

Receiving null value when using FirebaseMessaging.instance.getInitialMessage() for handling terminated state

I am developing an app using Flutter, and I am implementing push notification using FCM with the firebase_messaging: ^10.0.4 Flutter plugin:
I am using Firebase to send notification on mobile app while the mobile app is in terminated state. To get the notification in terminated state, I am using FirebaseMessaging.instance.getInitialMessage() to handle the on click of notification. When the user clicks on the notification, they will be routed to a specific screen (which shows the message passed).
The issue is I am getting the notification in mobile app in terminated state, but when I click on the notification, I am not routed to the specific screen which I passed in from Firebase and FirebaseMessaging.instance.getInitialMessage() value is getting null in message.
Please let me know If anyone have idea about this.
main.dart
checkFirebase() async {
await Firebase.initializeApp();
// Set the background messaging handler early on, as a named top-level function
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
if (!kIsWeb) {
channel = const AndroidNotificationChannel(
'high_importance_channel', // id
'High Importance Notifications', // title
description: 'This channel is used for important notifications.',
// description
importance: Importance.max,
);
flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
/// Create an Android Notification Channel.
///
/// We use this channel in the `AndroidManifest.xml` file to override the
/// default FCM channel to enable heads up notifications.
await flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>()
?.createNotificationChannel(channel);
/// Update the iOS foreground notification presentation options to allow
/// heads up notifications.showFrontNotification
await FirebaseMessaging.instance
.setForegroundNotificationPresentationOptions(
alert: true,
badge: true,
sound: true,
);
FirebaseCrashlytics.instance.setCrashlyticsCollectionEnabled(true);
}
}
class _ProcessAppState extends State<ProcessApp> {
Future<void> _initializeFuture;
Future<void> _initializeServices() async {
await Firebase.initializeApp();
await FirebaseCrashlytics.instance.setCrashlyticsCollectionEnabled(true);
var dir = await getApplicationDocumentsDirectory();
Hive.init(dir.path);
// pass all uncaught errors to crashlytics
Function originalOnError = FlutterError.onError;
FlutterError.onError = (FlutterErrorDetails errorDetails) async {
await FirebaseCrashlytics.instance.recordFlutterError(errorDetails);
originalOnError(errorDetails);
};
// Set the background messaging handler early on, as a named top-level function
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
if (!kIsWeb) {
channel = const AndroidNotificationChannel(
'high_importance_channel', // id
'High Importance Notifications', // title
description: 'This channel is used for important notifications.',
// description
importance: Importance.max,
);
/*
flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
/// Create an Android Notification Channel.
///
/// We use this channel in the `AndroidManifest.xml` file to override the
/// default FCM channel to enable heads up notifications.
await flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>()
?.createNotificationChannel(channel);
*/
/// Update the iOS foreground notification presentation options to allow
/// heads up notifications.
await FirebaseMessaging.instance
.setForegroundNotificationPresentationOptions(
alert: true,
badge: true,
sound: true,
);
}
}
#override
void initState() {
super.initState();
_initializeFuture = _initializeServices();
}
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
body: FutureBuilder(
future: _initializeFuture,
builder: (context, snapshot) {
if (snapshot.hasError) {
reportError(
error: snapshot.error, stackTrace: snapshot.stackTrace);
return Center(
child: Text(context.translateText(key: "general_error")),
);
}
if (snapshot.connectionState == ConnectionState.done) {
return MyApp();
}
return progressBar();
},
),
),
);
}
}
#override
Widget build(BuildContext context) {
return AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle(
statusBarColor: gradientTopColor,
statusBarBrightness: Brightness.dark,
),
child: MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: primaryColor,
backgroundColor: whiteColor,
fontFamily: 'Roboto'),
supportedLocales: [
Locale('en'),
Locale('ta'),
Locale('ml'),
Locale('kn'),
Locale('te'),
],
localizationsDelegates: [
// for our own localizations
AppLocalizations.delegate,
// localizations for all material widgets provided
GlobalMaterialLocalizations.delegate,
// localizations for all cupertino widgets provided
DefaultCupertinoLocalizations.delegate,
// for rtl, ltr text directions
GlobalWidgetsLocalizations.delegate,
],
locale: _locale,
localeResolutionCallback: (deviceLocale, supportedLocales) {
try {
return Locale(defaultLang);
} catch (e) {
print(e);
return Locale("en");
}
},
// navigation analytics reporting
// navigatorObservers: <NavigatorObserver>[observer],
home: NotificationMessageHandler(child: LauncherScreen()),
builder: EasyLoading.init(),
),
);
}
message_handler.dart
class _NotificationMessageHandlerState extends State<NotificationMessageHandler>
with AfterLayoutMixin<NotificationMessageHandler> {
#override
void initState() {
super.initState();
// _checkForUpdate();
var initializationSettingsAndroid =
AndroidInitializationSettings("#mipmap/ic_launcher");
var initializationSettings =
InitializationSettings(android: initializationSettingsAndroid);
flutterLocalNotificationsPlugin.initialize(initializationSettings,
onSelectNotification: (payload) async {
log("payload : $payload", name: "onSelectNotification");
handleNotificationClick(context, jsonDecode(payload));
});
FirebaseMessaging.instance
.getInitialMessage()
.then((RemoteMessage initialMessage) async {
log("message : $initialMessage", name: "getInitialMessage");
handleNotificationClick(context, jsonDecode(initialMessage.toString()));
});
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
print(message);
showFrontNotification(message);
});
FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
handleNotificationClick(context, message.data);
});
}
#override
Widget build(BuildContext context) {
return widget.child;
}
}
When your app is terminated and you want to navigate to another screen you need context from your MaterialApp's navigatorKey and also a keyword where you actually want to go, the keyword we will "click_action" key in the FCM request body.
I would recommend you handle your Firebase Messaging code in a separate file.
fcm_service.dart
This file contains route navigator in foreground, background and after app terminated, also handles notification with an image.
import 'dart:convert';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:http/http.dart' as http;
import '../main.dart';
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
const AndroidNotificationChannel channel = AndroidNotificationChannel(
'custom_notification_channel_id',
'Notification',
description: 'notifications from Your App Name.',
importance: Importance.high,
);
Future<void> firebaseMessagingBackgroundHandler(RemoteMessage message) async {
await Firebase.initializeApp();
}
void setupFcm() {
var initializationSettingsAndroid = const AndroidInitializationSettings('#mipmap/ic_launcher');
var initializationSettingsIOs = const IOSInitializationSettings();
var initializationSettings = InitializationSettings(
android: initializationSettingsAndroid,
iOS: initializationSettingsIOs,
);
//when the app is in foreground state and you click on notification.
flutterLocalNotificationsPlugin.initialize(initializationSettings,
onSelectNotification: (String payload) {
if (payload != null) {
Map<String, dynamic> data = json.decode(payload);
goToNextScreen(data);
}
});
//When the app is terminated, i.e., app is neither in foreground or background.
FirebaseMessaging.instance.getInitialMessage().then((RemoteMessage message) {
//Its compulsory to check if RemoteMessage instance is null or not.
if (message != null) {
goToNextScreen(message.data);
}
});
//When the app is in the background, but not terminated.
FirebaseMessaging.onMessageOpenedApp.listen((event) {
goToNextScreen(event.data);
},
cancelOnError: false,
onDone: () {},
);
FirebaseMessaging.onMessage.listen((RemoteMessage message) async {
RemoteNotification notification = message.notification;
AndroidNotification android = message.notification?.android;
if (notification != null && android != null) {
if (android.imageUrl != null && android.imageUrl.trim().isNotEmpty) {
final String largeIcon = await _base64encodedImage(
android.imageUrl,
);
final BigPictureStyleInformation bigPictureStyleInformation =
BigPictureStyleInformation(
ByteArrayAndroidBitmap.fromBase64String(largeIcon),
largeIcon: ByteArrayAndroidBitmap.fromBase64String(largeIcon),
contentTitle: notification.title,
htmlFormatContentTitle: true,
summaryText: notification.body,
htmlFormatSummaryText: true,
hideExpandedLargeIcon: true,
);
flutterLocalNotificationsPlugin.show(
notification.hashCode,
notification.title,
notification.body,
NotificationDetails(
android: AndroidNotificationDetails(
channel.id,
channel.name,
channelDescription: channel.description,
icon: 'custom_notification_icon',
color: primaryColor,
importance: Importance.max,
priority: Priority.high,
largeIcon: ByteArrayAndroidBitmap.fromBase64String(largeIcon),
styleInformation: bigPictureStyleInformation,
),
),
payload: json.encode(message.data),
);
}
else {
flutterLocalNotificationsPlugin.show(
notification.hashCode,
notification.title,
notification.body,
NotificationDetails(
android: AndroidNotificationDetails(
channel.id,
channel.name,
channelDescription: channel.description,
icon: 'custom_notification_icon',
color: primaryColor,
importance: Importance.max,
priority: Priority.high,
),
),
payload: json.encode(message.data),
);
}
}
});
}
Future<void> deleteFcmToken() async {
return await FirebaseMessaging.instance.deleteToken();
}
Future<String> getFcmToken() async {
String token = await FirebaseMessaging.instance.getToken();
return Future.value(token);
}
void goToNextScreen(Map<String, dynamic> data) {
if (data['click_action'] != null) {
switch (data['click_action']) {
case "first_screen":
navigatorKey.currentState.pushNamed(FirstScreen.routeName,);
break;
case "second_screen":
navigatorKey.currentState.pushNamed(SecondScreen.routeName,);
break;
case "sample_screen":
navigatorKey.currentState.pushNamed(SampleScreen.routeName,);
}
return;
}
//If the payload is empty or no click_action key found then go to Notification Screen if your app has one.
navigatorKey.currentState.pushNamed(NotificationPage.routeName,);
}
Future<String> _base64encodedImage(String url) async {
final http.Response response = await http.get(Uri.parse(url));
final String base64Data = base64Encode(response.bodyBytes);
return base64Data;
}
main.dart
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
FirebaseMessaging.onBackgroundMessage(firebaseMessagingBackgroundHandler);
await flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>()
?.createNotificationChannel(channel);
runApp(const MyApp());
}
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
class MyApp extends StatefulWidget {
const MyApp({Key key}) : super(key: key);
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
#override
void initState() {
super.initState();
setupFcm();
}
#override
Widget build(BuildContext context) {
return MaterialApp(
navigatorKey: navigatorKey,
onGenerateRoute: //Define your named routes.
);
}
}
Also, you need to define the default notification channel id, and optionally default notification icon, default notification color
AndroidManifest.xml
<application>
<meta-data
android:name="com.google.firebase.messaging.default_notification_channel_id"
android:value="custom_notification_channel_id" />
<!-- Set custom default icon. This is used when no icon is set for incoming notification messages. -->
<meta-data
android:name="com.google.firebase.messaging.default_notification_icon"
android:resource="#drawable/custom_notification_icon" />
<!-- Set color used with incoming notification messages. This is used when no color is set for the incoming notification message. -->
<meta-data
android:name="com.google.firebase.messaging.default_notification_color"
android:resource="#color/notification_icon_color" />
</application>
Also you can check sample FCM HTTP Request for handling "data" key JsonObject and "click_action" in it.
URL: https://fcm.googleapis.com/fcm/send
Request Method: POST
Header: {
"Authorization": "key={value1}",
"Sender": "id={value2}",
}
Request Body: {
"registration_ids": [
"exwlH32_S0il4ky4ZRXCrg:APA91bHp4kL-IJmtHRGFcQlhUauEY1ZiqZFfWsDkWqsB-yHDUzRVx63e8ehSirUTbSg6NqMqAAfcW16tk4dgs-NtTcCVShipGt9JWIJK_r8b4ldqFYGhzZcNF0VTiVKWzWkRQQIncCoE"
],
"notification": {
"title": "Wear Mask",
"body": "Maintain social distance",
"image": "https://repository-images.githubusercontent.com/31792824/fb7e5700-6ccc-11e9-83fe-f602e1e1a9f1",
"imageUrl": "https://repository-images.githubusercontent.com/31792824/fb7e5700-6ccc-11e9-83fe-f602e1e1a9f1",
"sound": "default"
},
"data": {
"click_action": "sample_screen",
"custom_key": "custom_value",
"image": "https://repository-images.githubusercontent.com/31792824/fb7e5700-6ccc-11e9-83fe-f602e1e1a9f1",
"imageUrl": "https://repository-images.githubusercontent.com/31792824/fb7e5700-6ccc-11e9-83fe-f602e1e1a9f1"
}
}
Note: registration_ids key only takes 1000 values in the list.
In "data" JsonObject you can define your custom key-value pair, which will come in handy. e.g., you want to open a specific screen let's say an event_screen.dart and you need to fetch the event details from the server by event id. So you can prepare your "data" object accordingly
"data": {
"click_action": "event_screen",
"event_id": "23"
}
I know I am late to the party but may this help someone in future. I was facing the same issue and I used the above answer by Smeet Bhatt but I was still facing the same issue. What I noticed that I was clicking too fast may be await Firebase.initializeApp(); was not finished yet. When I waited for a while, RemoteMessage was never null on FirebaseMessaging.instance.getInitialMessage().

Firebase messaging - Notifications are not sent when app is Closed or sleep in Flutter

I have built my app in flutter and I have implemented both LocalNotifications and FCM messaging.
this is my code:
final FirebaseMessaging firebaseMessaging = FirebaseMessaging();
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
FlutterLocalNotificationsPlugin();
#override
void initState() {
super.initState();
registerNotification();
configLocalNotification();
}
void registerNotification() {
firebaseMessaging.requestNotificationPermissions();
firebaseMessaging.configure(onMessage: (Map<String, dynamic> message) {
print('onMessage: $message');
return ;
}, onResume: (Map<String, dynamic> message) {
print('onResume: $message');
return Navigator.push(
context,
MaterialPageRoute(
builder: (context) => NotificationsScreen()));
},
onLaunch: (Map<String, dynamic> message) {
print('onLaunch: $message');
return;
});
firebaseMessaging.getToken().then((token) {
print('token: $token');
FirebaseFirestore.instance
.collection('Consultant')
.doc(firebaseUser.uid)
.update({'deviceToken': token});
}).catchError((err) {
//Fluttertoast.showToast(msg: err.message.toString());
});
}
Future selectNotification(String payload) async {
if (payload != null) {
debugPrint('notification payload: $payload');
}
await Navigator.push(
context,
MaterialPageRoute<void>(builder: (context) => NotificationsScreen(payload: payload,)),
);
}
void showNotification(message) async {
var androidPlatformChannelSpecifics = new AndroidNotificationDetails(
Platform.isAndroid
? 'it.wytex.vibeland_pro_app'
: 'it.wytex.vibeland_pro_app',
'Vibeland Pro',
'Vibeland Pro',
playSound: true,
enableVibration: true,
importance: Importance.max,
priority: Priority.high,
);
var iOSPlatformChannelSpecifics = new IOSNotificationDetails();
var platformChannelSpecifics = new NotificationDetails(
android: androidPlatformChannelSpecifics, iOS: iOSPlatformChannelSpecifics);
print(message);
print(message['body'].toString());
print(json.encode(message));
await flutterLocalNotificationsPlugin.show(0, message['title'].toString(),
message['body'].toString(), platformChannelSpecifics,
payload: json.encode(message));
await flutterLocalNotificationsPlugin.show(
1, '📩 Hai ricevuto un messaggio 📩 ', 'Controlla subito le Tue notifiche 🔔🔔', platformChannelSpecifics,
payload: 'item x',
);
}
void configLocalNotification() {
var initializationSettingsAndroid =
new AndroidInitializationSettings('#mipmap/ic_launcher');
var initializationSettingsIOS = new IOSInitializationSettings(
requestAlertPermission: true,
requestBadgePermission: true,
requestSoundPermission: true,
);
var initializationSettings = new InitializationSettings(
android: initializationSettingsAndroid, iOS: initializationSettingsIOS);
flutterLocalNotificationsPlugin.initialize(initializationSettings);
}
I have built a function in firebase to push some New collections as notifications.
const functions = require("firebase-functions");
const admin = require("firebase-admin");
admin.initializeApp(functions.config().firebase);
const fcm = admin.messaging();
exports.sendNotification = functions.firestore
.document("Notifications/{id}")
.onCreate((snapshot) => {
const name = snapshot.get("name");
const subject = snapshot.get("subject");
const token = snapshot.get("token");
const payload = {
notification: {
title: "" + name,
body: "" + subject,
sound: "default",
click_action: "FLUTTER_NOTIFICATION_CLICK",
},
};
return fcm.sendToDevice(token, payload);
});
the version of firebase_messaging: ^7.0.3 and flutter_local_notifications: ^4.0.1
at the moment I don't upgrade due to some conflict with dependencies.
In this way I got both notifications when an app is open I get Local notifications correctly and when an app is in foreground and background I get Firebasemessaging according to the new collection added into my firestore.
The problem now comes when I close the app or the app after some minutes starts to sleep...
I can't get any notifications
To start again to get notifications, I need to run again the app and wake the app.
This is a problem with my app because my Apps notifications are very important and users need to get them always.
As you can on the FlutterFire documentation, foreground and background notification are handled differently by the plugin, so there are 2 thing you need to fix in your app.
First you need to prepare your Cloud Function to send background notifications as well as foreground, and in order to to that, you need to prepare your json to not only have a notification but also a data field, as follows:
const payload = {
notification: {
title: "" + name,
body: "" + subject,
sound: "default",
},
data: {
click_action: "FLUTTER_NOTIFICATION_CLICK"
}
};
Second, you are going to need configure your firebaseMassaging to receive background messages, like this:
firebaseMessaging.configure(
onMessage: (Map<String, dynamic> message) {
print('onMessage: $message');
return ;
},
onResume: (Map<String, dynamic> message) {
print('onResume: $message');
return Navigator.push(
context,
MaterialPageRoute(
builder: (context) => NotificationsScreen()));
},
onLaunch: (Map<String, dynamic> message) {
print('onLaunch: $message');
return;
},
onBackgroundMessage: myBackgroundMessageHandler
);
And finally you need to create a handler that will manually handle background messages, following the example in the documentation you can do something like this:
Future<void> myBackgroundMessageHandler(RemoteMessage message) async {
print("Handling a background message: ${message}");
}
Adding the Line:
onBackgroundMessage: myBackgroundMessageHandler
Future<void> myBackgroundMessageHandler(RemoteMessage message) async {
print("Handling a background message: ${message}");
}
I got these error:
Nexus, [10.03.21 16:59]
[ File : app-release.apk ]
Amore, [10.03.21 17:09]
java.lang.RuntimeException: Unable to create service io.flutter.plugins.firebasemessaging.FlutterFirebaseMessagingService: java.lang.RuntimeException: PluginRegistrantCallback is not set.
at android.app.ActivityThread.handleCreateService(ActivityThread.java:4023)
at android.app.ActivityThread.access$1600(ActivityThread.java:224)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1903)
at android.os.Handler.dispatchMessage(Handler.java:107)
at android.os.Looper.loop(Looper.java:224)
at android.app.ActivityThread.main(ActivityThread.java:7562)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:539)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:950)
Caused by: java.lang.RuntimeException: PluginRegistrantCallback is not set.
at io.flutter.plugins.firebasemessaging.FlutterFirebaseMessagingService.C(Unknown Source:70)
at io.flutter.plugins.firebasemessaging.FlutterFirebaseMessagingService.onCreate(Unknown Source:40)
at android.app.ActivityThread.handleCreateService(ActivityThread.java:4011)
... 8 more

Flutter - Firebase Cloud Messaging Navigation in onLaunch doesn't work

I am building an app which receives push notifications using FCM.
I want to route to a specific screen when a notification is clicked (for example, the user's profile).
On Android, it works perfectly fine when the app is just closed (and not "killed"), but when the app is terminated ("killed") it is not working.
On iOS, it doesn't work at all.
I am implementing it life this:
NotificationsHandler:
class NotificationsHandler {
static final NotificationsHandler instance = NotificationsHandler();
final _fcm = FirebaseMessaging();
void onBackgroundNotificationRecevied({Function onReceived}) {
_fcm.configure(
onResume: (message) => onReceived(message),
onLaunch: (message) => onReceived(message),
);
}
}
myMainScreen's initState:
#override
void initState() {
NotificationsHandler.instance.onBackgroundNotificationRecevied(
onReceived: (message) async {
final userId = message['data']['userId'];
final user = this.users.firstWhere((currentUser) => currentUser.id == userId);
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => UserProfileScreen(
user,
),
),
);
}
);
super.initState();
}
Code for sending the notifications (through an external React admin panel):
const payload = {
notification: {
title: `myTitle`,
body: `My message`,
sound: "default",
badge: "1",
click_action: "FLUTTER_NOTIFICATION_CLICK",
},
data: {
click_action: 'FLUTTER_NOTIFICATION_CLICK',
userId: myUserId,
},
};
const options = {
priority: 'high',
timeToLive: 60 * 60 * 24
};
admin.messaging().sendToTopic('myTopic', payload, options);
Does anyone know why it isn't working?
Thank you!
You can try to use getInitialMessage instead of onLaunch. I believe this will do what you want as documentation indicated the following lines:
This should be used to determine whether specific notification interaction should open the app with a specific purpose (e.g. opening a chat message, specific screen etc).
#override
void initState() {
super.initState();
FirebaseMessaging.instance.getInitialMessage().then((RemoteMessage message) {
if (message != null) {
Navigator.pushNamed(context, '/message', arguments: MessageArguments(message, true));
}
});
}
I assume that you're using firebase_messaging package.
iOS
If you're testing it on simulator, it won't work. It's stated in the documentation that:
FCM via APNs does not work on iOS Simulators. To receive messages & notifications a real device is required.
Android
On Android, if the user force quits the app from device settings, it must be manually reopened again for messages to start working.
More info here.
Based on my experience, I remember that onLaunch Callback function fires right after execute main function, even before the initstate method.
What I did was locate service class using service locator(e.g get_it) at main function before runApp() then onLaunch Callback set initial configuration so that your App can use it's value.
For example
final getIt = GetIt.instance;
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
getIt.registerSingleton<Configurator>(Configurator());///start configuration service
FirebaseMessagingService.initialise()///start firebase messaging service
runApp();
}
...
class FirebaseMessagingService {
final FirebaseMessaging _fcm;
FirebaseMessagingService.initialise() : _fcm = FirebaseMessaging() {
if (Platform.isIOS) {
_fcm.requestNotificationPermissions(IosNotificationSettings());
}
_fcm.configure(
...
onLaunch: _launchMessageHandler,
);
}
}
//top-level function or static method
_launchMessageHandler(Map<String, dynamic> message) async {
//some parsing logic
...
getIt<Configurator>().setInitialConfig(parsed_data);
}
...
//then
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
final config = getIt<Configurator>().config;
//do something
}};
You will have to implement those whole settings but it's flow is like above roughly.
I assume your trouble is more towards navigating to another screen upon clicking the notification.
If that is the case create a class for routing.
an example would be as below:
class Navigator{
GlobalKey<NavigatorState> _navigator;
/// Singleton getter
static Navigator get instance => _instance ??= Navigator._();
/// Singleton Holder
static Navigator _instance;
/// Private Constructor
Navigator._() {
_navigator = GlobalKey<NavigatorState>();
}
GlobalKey<NavigatorState> get navigatorKey => _navigator;
Future<dynamic> navigateTo(String routeName, [dynamic arguments]) =>
navigatorKey.currentState.pushNamed(routeName, arguments: arguments);
Now comes the screen/pages
class CustomRoutes {
const CustomRoutes._();
factory CustomRoutes() => CustomRoutes._();
static const String HomeRoute = 'HomeRoute';
...
...
static Route<dynamic> generateRoute(RouteSettings settings) {
switch (settings.name) {
case CustomRoutes.HomeRoute:
return MaterialPageRoute(builder: (_) => HomePage());
default:
return MaterialPageRoute(
builder: (_) => Scaffold(
body: Center(child: Text('No path for ${settings.name}'))));
}
}
}
So if u wish to go to HomePage you can just invoke
await Navigator.instance.navigateTo(CustomRoutes.HomeRoute, someArguments)
Do remember to register the globalkey to your materialapp
MaterialApp(
...
...
navigatorKey: Navigator.instance.navigatorKey
...);

TypeError:Cannot read property 'androidNotificationToken' of undefined exports.onMessageSent.functions.firestore.document.onCreate(/workspace/index.js

EDITED onMessageSent function. Still the same error.
I'm trying to enable push notifications in flutter and I am using Firebase Messaging. I am encountering the following issue. There are two, namely "onCreateActivityFeedItem" & "onMessageSent".
For the first one, "onCreateActivityFeedItem", the notification feature is perfectly fine, but I could not recognize the problem with the second one. Please do help.
The problem I am facing :
onMessageSent
TypeError: Cannot read property 'androidNotificationToken' of undefined at exports.onMessageSent.functions.firestore.document.onCreate (/workspace/index.js:152:47) at process._tickCallback (internal/process/next_tick.js:68:7)
Here is the "onCreateActivityFeedItem" :
From my index.js
exports.onCreateActivityFeedItem = functions.firestore
.document("/feed/{userId}/feedItems/{activityFeedItem}")
.onCreate(async (snapshot, context) => {
console.log("Activity Feed Item Created", snapshot.data());
// 1) Get user connected to the feed
const userId = context.params.userId;
const mediaUrl=context.params.mediaUrl;
const userRef = admin.firestore().doc(`users/${userId}`);
const doc = await userRef.get();
// 2) Once we have user, check if they have a notification token; send notification, if they have a token
const androidNotificationToken = doc.data().androidNotificationToken;
const createdActivityFeedItem = snapshot.data();
if (androidNotificationToken) {
sendNotification(androidNotificationToken, createdActivityFeedItem);
} else {
console.log("No token for user, cannot send notification");
}
function sendNotification(androidNotificationToken, activityFeedItem) {
let body;
// 3) switch body value based off of notification type
switch (activityFeedItem.type) {
case "comment":
body = `${activityFeedItem.username} replied: ${
activityFeedItem.commentData
}.`;
break;
case "like":
body = `${activityFeedItem.username} booped you.`;
break;
case "follow":
body = `${activityFeedItem.username} started petting you.`;
break;
default:
break;
}
// 4) Create message for push notification
const message = {
notification: {
body:body,
image:mediaUrl
},
token: androidNotificationToken,
data: {recipient: userId,
}
};
// 5) Send message with admin.messaging()
admin
.messaging()
.send(message)
.then(response => {
// Response is a message ID string
console.log("Successfully sent message", response);
return null;
})
.catch(error => {
console.log("Successfully sent message", response);
throw Error("Could not send message.",error)});
// admin.messaging().sendToDevice(androidNotificationToken,message);
}
});
From my index.js
Here is the, "onMessageSent":
exports.onMessageSent = functions.firestore
.document('/messages/{chatId}/messageInfo/{messageFeedItem}')
.onCreate(async (snapshot, context) => {
console.log("Message Created", snapshot.data());
// 1) Get user connected to the feed
const chatId=context.params.chatId;
const userId = context.params.idTo;
const idTo =context.params.idTo;
const userRef = admin.firestore().doc(`users/${idTo}`);
const doc = await userRef.get();
const createdMessageFeedItem = snapshot.data();
// 2) Once we have user, check if they have a notification token; send notification, if they have a token
const androidNotificationToken = doc.data().androidNotificationToken;
if (androidNotificationToken) {
sendNotification(androidNotificationToken, createdMessageFeedItem);
} else {
console.log("No token for user, cannot send notification");
}
function sendNotification(androidNotificationToken,createdMessageFeedItem) {
let body;
// 3) switch body value based off of notification type
switch (messageFeedItem.type) {
case 0:
body = `${messageFeedItem.username} has sent a message : ${
messageFeedItem.content
}.`;
break;
case 1:
body = `${messageFeedItem.username} has sent an image.`;
break;
case 2:
body = `${messageFeedItem.username} has sent a gif.`;
break;
default:
break;
}
// 4) Create message for push notification
const message = {
notification:
{body:body,},
token: androidNotificationToken,
data: {recipient: idTo,}
};
// 5) Send message with admin.messaging()
admin
.messaging()
.send(androidNotificationToken,message)
.then(response => {
// Response is a message ID string
console.log("Successfully sent message", response);
return null;
})
.catch(error => {
console.log("Successfully sent message", response);
throw Error("Could not send message.",error)});
// admin.messaging().sendToDevice(androidNotificationToken,message);
}
});
Where I called/declared the onMessage, onResume :
configurePushNotifications() {
final GoogleSignInAccount user = googleSignIn.currentUser;
if (Platform.isIOS) {
getiOSPermission();
}
_firebaseMessaging.getToken().then((token) {
print("Firebase messaging token : $token");
setState(() {
currentUser.androidNotificationToken = token;
});
usersref.doc(user.id).update({"androidNotificationToken": token});
});
_firebaseMessaging.configure(
onLaunch: (Map<String, dynamic> message) async {
_firebaseMessaging.getToken().then((token) {
print("Firebase messaging token : $token");
usersref.doc(user.id).update({"androidNotificationToken": token});
});
// print("On Launch : $message\n");
// _navigateToDetail(message);
},
onResume: (Map<String, dynamic> message) async {
_firebaseMessaging.getToken().then((token) {
print("Firebase messaging token : $token");
usersref.doc(user.id).update({"androidNotificationToken": token});
});
print("On Resume : $message");
_navigateToDetail(message);
},
onMessage: (Map<String, dynamic> message) async {
print("On message : $message\n");
final String recipientId = message['data']['recipient'];
final String body = message['notification']['body'];
if (recipientId == user.id) {
//Notification shown");
SnackBar snackBar = SnackBar(
backgroundColor: Colors.blueAccent,
content: Text(
body,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.w600,
),
),
action: SnackBarAction(
label: "Go",
textColor: Colors.black,
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) {
return ActivityFeed();
}),
);
}),
);
_scaffoldKey.currentState.showSnackBar(snackBar);
}
//Notifications not shown.");
},
);
}
I tried different ways like getting the androidNotificationToken by updating in cloud firestore and getting it, but it didn't work.
[Users in Cloud Firestore][1]
[1] https://imgur.com/a/u5Df0zD
I'm just a beginner, trying to learn new stuff. Please do help.
Thank you,
SLN
Cannot read type error of undefined
error happens when the data dosent exist at docu.data().androidNotificationToken
Ensure the reference is correct, I prefer using this type of path for more clarity
let ref = db.collection(‘users’).doc(userID);etc
You can also catch a empty snapshot
var snapshot = await ref.get();
if(snapshot.empty){
console.log(‘snapshot is empty’);
}

Resources