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"
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With