I'm using Firebase Cloud Messaging (FCM) in my Flutter app and trying to achieve the following:
Play a custom notification sound when a notification is received. Redirect to a specific screen when the notification is clicked, even if the app is terminated or not running in the background.
Issues:
Custom Sound Problem:
- When I include the notification object in the payload, the notification plays the default sound instead of the custom sound.
- If I include both notification and custom sound configuration in the android
section, I receive two notifications:
- One with the default sound.
- Another with the custom sound.
Redirection Problem:
- When I remove the notification object and rely entirely on the data object, the custom sound works, but redirection fails if the app is terminated or not in the background.
- The app doesn't receive the notification click event in such cases.
Payload I'm Using:
Below is the payload I'm sending to FCM:
{
"message": {
"token": "DEVICE_FCM_TOKEN",
"notification": {
"title": "Visitor has been admitted!",
"body": "Dhaval developer (Visitor) has been admitted.",
},
"android": {
"notification": {
"sound": "visitor_notification_sound"
}
},
"apns":{
"payload":{
"aps":{
"sound":"visitor_notification_sound.mp3"
}
}
},
"data": {
"id": "1215454",
"notification_type": "visitor_visited",
"other_data_key": "other_data_value"
}
}
}
Observations:
With the notification object:
- Redirection works even when the app is terminated or not in the background.
- The notification plays the default sound, not the custom sound.
Without the notification object:
- The custom sound works fine.
- Redirection fails when the app is terminated or not in the background (click events are not received).
Including both notification and custom sound in the android section:
- Two notifications are received:
- One with the default sound.
- Another with the custom sound.
- Two notifications are received:
Flutter Code: Here’s how I’m handling notifications in my Flutter app:
main.dart
import 'package:flutter/material.dart';
import 'package:notification_demo/fcm_controller.dart';
import 'package:notification_demo/firebase_options.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
await FirebaseCloudMessagingService().initFCM();
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: Scaffold(
body: Center(
child: Text('hello'),
),
),
);
}
}
fcm_controller.dart
import 'dart:convert';
import 'dart:developer';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'dart:math' as math;
import 'package:notification_demo/firebase_options.dart';
@pragma('vm:entry-point')
Future<void> notificationTapBackground(NotificationResponse notificationResponse) async {
debugPrint('background notification tap');
}
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
@pragma('vm:entry-point')
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
debugPrint("Handling a background message ${message.messageId}");
if (message.messageId != null && message.messageId!.isNotEmpty) {
FirebaseCloudMessagingService.showNotification(message);
}
}
class FirebaseCloudMessagingService {
Future<void> initFCM() async {
debugPrint('DDDD initFCM');
try {
await _requestPermission();
await _initNotificationInfo();
String deviceToken = await _getToken() ?? '';
debugPrint('FCM Token: $deviceToken');
await _setupMessageHandlers();
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
} catch (e) {
log("Exception: $e");
}
}
Future<void> _requestPermission() async {
NotificationSettings settings = await FirebaseMessaging.instance.requestPermission(
alert: true,
announcement: true,
badge: true,
carPlay: true,
criticalAlert: true,
provisional: true,
sound: true,
);
debugPrint('Permission status: ${settings.authorizationStatus}');
}
Future<void> _initNotificationInfo() async {
var initializationSettingAndroid = const AndroidInitializationSettings('@mipmap/ic_launcher');
const DarwinInitializationSettings initializationSettingIOS = DarwinInitializationSettings();
var initializationSettings = InitializationSettings(android: initializationSettingAndroid, iOS: initializationSettingIOS);
await flutterLocalNotificationsPlugin.initialize(initializationSettings, onDidReceiveNotificationResponse: (NotificationResponse notificationResponse) async {
handleNotificationTappedFormNotificationTray(jsonDecode(notificationResponse.payload ?? "{}"));
}, onDidReceiveBackgroundNotificationResponse: notificationTapBackground);
}
Future<String?> _getToken() async {
try {
return await FirebaseMessaging.instance.getToken();
} catch (e) {
debugPrint("Error fetching token: $e");
return null;
}
}
Future<void> _setupMessageHandlers() async {
FirebaseMessaging.onMessage.listen((RemoteMessage message) async {
showNotification(message);
});
FirebaseMessaging.onMessageOpenedApp.listen((event) async {
await handleNotificationTappedFormNotificationTray(event.data);
});
}
static Future<void> showNotification(RemoteMessage message) async {
String title = message.data['title'] ?? '';
String body = message.data['body'] ?? '';
String soundName = 'notification_sound_android';
String iosSoundName = 'notification_sound_android.mp3';
if (message.data['notification_type'] == 'visitor_visited') {
soundName = 'visitor_notification_sound';
iosSoundName = 'visitor_notification_sound.mp3';
}
AndroidNotificationChannel channel = AndroidNotificationChannel(
soundName,
'General Notifications',
importance: Importance.max,
playSound: true,
sound: RawResourceAndroidNotificationSound(soundName),
);
AndroidNotificationDetails androidNotificationDetails = AndroidNotificationDetails(
channel.id,
channel.name,
sound: RawResourceAndroidNotificationSound(soundName),
);
NotificationDetails notificationDetails = NotificationDetails(
android: androidNotificationDetails,
iOS: DarwinNotificationDetails(sound: iosSoundName),
);
flutterLocalNotificationsPlugin.show(
math.Random().nextInt(100000),
title,
body,
notificationDetails,
payload: jsonEncode(message.data),
);
}
Future<void> handleNotificationTappedFormNotificationTray(Map<String, dynamic> notificationData) async {
debugPrint('Notification tapped: $notificationData');
// Implement redirection logic here
}
}
Question: How can I configure FCM and handle notifications in Flutter so that:
- Custom sound plays without triggering the default sound or duplicating notifications.
- Clicking the notification redirects to a specific screen, even if the app is terminated or not running in the background.
- Only one notification is displayed.
Is there a way to resolve the conflict between the notification and data objects to achieve the desired behavior?
I'm using Firebase Cloud Messaging (FCM) in my Flutter app and trying to achieve the following:
Play a custom notification sound when a notification is received. Redirect to a specific screen when the notification is clicked, even if the app is terminated or not running in the background.
Issues:
Custom Sound Problem:
- When I include the notification object in the payload, the notification plays the default sound instead of the custom sound.
- If I include both notification and custom sound configuration in the android
section, I receive two notifications:
- One with the default sound.
- Another with the custom sound.
Redirection Problem:
- When I remove the notification object and rely entirely on the data object, the custom sound works, but redirection fails if the app is terminated or not in the background.
- The app doesn't receive the notification click event in such cases.
Payload I'm Using:
Below is the payload I'm sending to FCM:
{
"message": {
"token": "DEVICE_FCM_TOKEN",
"notification": {
"title": "Visitor has been admitted!",
"body": "Dhaval developer (Visitor) has been admitted.",
},
"android": {
"notification": {
"sound": "visitor_notification_sound"
}
},
"apns":{
"payload":{
"aps":{
"sound":"visitor_notification_sound.mp3"
}
}
},
"data": {
"id": "1215454",
"notification_type": "visitor_visited",
"other_data_key": "other_data_value"
}
}
}
Observations:
With the notification object:
- Redirection works even when the app is terminated or not in the background.
- The notification plays the default sound, not the custom sound.
Without the notification object:
- The custom sound works fine.
- Redirection fails when the app is terminated or not in the background (click events are not received).
Including both notification and custom sound in the android section:
- Two notifications are received:
- One with the default sound.
- Another with the custom sound.
- Two notifications are received:
Flutter Code: Here’s how I’m handling notifications in my Flutter app:
main.dart
import 'package:flutter/material.dart';
import 'package:notification_demo/fcm_controller.dart';
import 'package:notification_demo/firebase_options.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
await FirebaseCloudMessagingService().initFCM();
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: Scaffold(
body: Center(
child: Text('hello'),
),
),
);
}
}
fcm_controller.dart
import 'dart:convert';
import 'dart:developer';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'dart:math' as math;
import 'package:notification_demo/firebase_options.dart';
@pragma('vm:entry-point')
Future<void> notificationTapBackground(NotificationResponse notificationResponse) async {
debugPrint('background notification tap');
}
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
@pragma('vm:entry-point')
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
debugPrint("Handling a background message ${message.messageId}");
if (message.messageId != null && message.messageId!.isNotEmpty) {
FirebaseCloudMessagingService.showNotification(message);
}
}
class FirebaseCloudMessagingService {
Future<void> initFCM() async {
debugPrint('DDDD initFCM');
try {
await _requestPermission();
await _initNotificationInfo();
String deviceToken = await _getToken() ?? '';
debugPrint('FCM Token: $deviceToken');
await _setupMessageHandlers();
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
} catch (e) {
log("Exception: $e");
}
}
Future<void> _requestPermission() async {
NotificationSettings settings = await FirebaseMessaging.instance.requestPermission(
alert: true,
announcement: true,
badge: true,
carPlay: true,
criticalAlert: true,
provisional: true,
sound: true,
);
debugPrint('Permission status: ${settings.authorizationStatus}');
}
Future<void> _initNotificationInfo() async {
var initializationSettingAndroid = const AndroidInitializationSettings('@mipmap/ic_launcher');
const DarwinInitializationSettings initializationSettingIOS = DarwinInitializationSettings();
var initializationSettings = InitializationSettings(android: initializationSettingAndroid, iOS: initializationSettingIOS);
await flutterLocalNotificationsPlugin.initialize(initializationSettings, onDidReceiveNotificationResponse: (NotificationResponse notificationResponse) async {
handleNotificationTappedFormNotificationTray(jsonDecode(notificationResponse.payload ?? "{}"));
}, onDidReceiveBackgroundNotificationResponse: notificationTapBackground);
}
Future<String?> _getToken() async {
try {
return await FirebaseMessaging.instance.getToken();
} catch (e) {
debugPrint("Error fetching token: $e");
return null;
}
}
Future<void> _setupMessageHandlers() async {
FirebaseMessaging.onMessage.listen((RemoteMessage message) async {
showNotification(message);
});
FirebaseMessaging.onMessageOpenedApp.listen((event) async {
await handleNotificationTappedFormNotificationTray(event.data);
});
}
static Future<void> showNotification(RemoteMessage message) async {
String title = message.data['title'] ?? '';
String body = message.data['body'] ?? '';
String soundName = 'notification_sound_android';
String iosSoundName = 'notification_sound_android.mp3';
if (message.data['notification_type'] == 'visitor_visited') {
soundName = 'visitor_notification_sound';
iosSoundName = 'visitor_notification_sound.mp3';
}
AndroidNotificationChannel channel = AndroidNotificationChannel(
soundName,
'General Notifications',
importance: Importance.max,
playSound: true,
sound: RawResourceAndroidNotificationSound(soundName),
);
AndroidNotificationDetails androidNotificationDetails = AndroidNotificationDetails(
channel.id,
channel.name,
sound: RawResourceAndroidNotificationSound(soundName),
);
NotificationDetails notificationDetails = NotificationDetails(
android: androidNotificationDetails,
iOS: DarwinNotificationDetails(sound: iosSoundName),
);
flutterLocalNotificationsPlugin.show(
math.Random().nextInt(100000),
title,
body,
notificationDetails,
payload: jsonEncode(message.data),
);
}
Future<void> handleNotificationTappedFormNotificationTray(Map<String, dynamic> notificationData) async {
debugPrint('Notification tapped: $notificationData');
// Implement redirection logic here
}
}
Question: How can I configure FCM and handle notifications in Flutter so that:
- Custom sound plays without triggering the default sound or duplicating notifications.
- Clicking the notification redirects to a specific screen, even if the app is terminated or not running in the background.
- Only one notification is displayed.
Is there a way to resolve the conflict between the notification and data objects to achieve the desired behavior?
Share Improve this question asked Jan 18 at 14:05 codnix devcodnix dev 132 bronze badges1 Answer
Reset to default 0Well i do have this problems:
1.Custom sound plays without triggering the default sound or duplicating notifications.
- Try to physical device some devices/emulator might not work. Especially on XIAOMI phone.
2.Clicking the notification redirects to a specific screen, even if the app is terminated or not running in the background.
On flutter_local_notification have a default when clicking the notification goes to the app, try playing this code: NOTE put this with initializing the _flutterLocalNotificationsPlugin1.initialize
final NotificationAppLaunchDetails? notificationAppLaunchDetails =
await _flutterLocalNotificationsPlugin1
.getNotificationAppLaunchDetails();
final didNotificationLaunchApp =
notificationAppLaunchDetails?.didNotificationLaunchApp ?? false;
if (didNotificationLaunchApp == true) {
/// GET DATA
final dataX = notificationAppLaunchDetails?.notificationResponse!.payload;
Future.delayed(const Duration(seconds: 2), () async {
/// PUT CODE HERE TO REDIRECT
});
// throw Exception('$dataX');
}
as am using go_ router for page navigation as per my code when app is clicked or redirect or scan in qr.
- Only one notification is displayed.
Well on Firebase we have 2 ways to send payload with data and only the notification with body.
- As am using is only the data. As per my condition i adjust the data to put the title and body inside so am manually fetching those. Since once on terminate i received two notification due the other notification don't have the data so that's why.