Flutter后台/进程杀死状态下Firebase通知重复问题
问题:后台/进程杀死状态下Firebase通知重复显示
我正在开发一款Flutter应用,需在后台及进程杀死状态下接收通知。采用Firebase Messaging与Flutter Local Notification实现通知持久化,但当前在后台和进程杀死状态下会收到两条重复通知。尝试将后端推送改为纯data payload后,仍存在重复问题,仅其中一条通知无标题与内容。
代码实现
import 'dart:developer'; import 'package:eaglet/core/router/router_constants.dart'; import 'package:eaglet/presentation/blocs/auth_bloc/auth_bloc.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:go_router/go_router.dart'; import '../../firebase_options.dart'; import '../../my_app.dart'; class FirebaseService { static final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); static AndroidNotificationChannel generalChannel = AndroidNotificationChannel( 'GENERAL', 'General Notifications', description: 'General app notifications', importance: Importance.max, playSound: true, sound: RawResourceAndroidNotificationSound('general'), ); static AndroidNotificationChannel incidentChannel = AndroidNotificationChannel( 'ALARM', 'Alarm Notifications', description: 'Incident alerts', importance: Importance.max, playSound: true, enableLights: true, sound: RawResourceAndroidNotificationSound('notification'), ); static Future<void> init() async { try { WidgetsFlutterBinding.ensureInitialized(); final app = await Firebase.initializeApp( options: DefaultFirebaseOptions.currentPlatform, ); log("Firebase App Initialized: $app"); await Future.delayed(const Duration(seconds: 1)); await FirebaseMessaging.instance.setAutoInitEnabled(true); await FirebaseMessaging.instance .setForegroundNotificationPresentationOptions( alert: true, badge: true, sound: true, ); const DarwinInitializationSettings initializationSettingsIOS = DarwinInitializationSettings( requestAlertPermission: true, requestBadgePermission: true, requestSoundPermission: true, ); final InitializationSettings initializationSettings = InitializationSettings(iOS: initializationSettingsIOS); flutterLocalNotificationsPlugin.initialize(initializationSettings); final androidPlugin = flutterLocalNotificationsPlugin.resolvePlatformSpecificImplementation< AndroidFlutterLocalNotificationsPlugin>(); await androidPlugin?.createNotificationChannel(incidentChannel); await androidPlugin?.createNotificationChannel(generalChannel); // Register Background Message Handler // FirebaseMessaging.onBackgroundMessage(firebaseMessagingBackgroundHandler); } catch (e) { log("FirebaseService.init Error: $e"); } } static Future<void> initTokenChange(BuildContext context) async { FirebaseMessaging.instance.onTokenRefresh.listen((fcmToken) async { log("FCM TOKEN CHANGE: $fcmToken"); context.read<AuthBloc>().add(FCMTokenUpdateEvent(fcmToken)); }).onError((err) { log("Error in token refresh: $err"); }); } static Future<void> initInAPPNotification(BuildContext context) async { var initializationSettings = const InitializationSettings( android: AndroidInitializationSettings('@drawable/ic_notification'), iOS: DarwinInitializationSettings( requestAlertPermission: true, requestBadgePermission: true, requestSoundPermission: true, requestCriticalPermission: true ), ); flutterLocalNotificationsPlugin.initialize( initializationSettings, onDidReceiveNotificationResponse: (details) { log("Alarm redirection 105-- ${details.payload}"); if (details.payload != null && details.payload!.isNotEmpty) { context.pushNamed(AppRouteConstants.alarmScreenPageRoute.name, extra: details.payload); } }, ); FirebaseMessaging.onBackgroundMessage(firebaseMessagingBackgroundHandler); FirebaseMessaging.onMessage.listen((RemoteMessage message) async { //final notification = message.notification; log('onMessage Notification notification title: ${message.notification?.title}'); log('onMessage Notification Data: ${message.data}'); if (message.data.isEmpty) { log('Notification was null'); return; } if (message.data['type'] == "INCIDENT") { log("Alarm redirection 124-- ${message.data}"); globalNavigatorKey.currentContext?.pushNamed( AppRouteConstants.alarmScreenPageRoute.name, extra: message.data['incidentId']); } if (message.data['type'] == "INCIDENT" || message.data['type'] == "TEST") { log("foreground message"); await flutterLocalNotificationsPlugin.show( 0, message.data['title'], message.data['body'], NotificationDetails( android: AndroidNotificationDetails( incidentChannel.id, incidentChannel.name, channelDescription: incidentChannel.description, importance: incidentChannel.importance, priority: Priority.max, fullScreenIntent: true, playSound: true, enableLights: true, autoCancel: message.data['type'] == "INCIDENT" ? false : true, ongoing: message.data['type'] == "INCIDENT" ? true : false, visibility: NotificationVisibility.public, sound: incidentChannel.sound, icon: "ic_notification", color: const Color(0xFFCC0000), ), iOS: DarwinNotificationDetails( presentAlert: true, // Show alert even if in foreground presentBadge: true, presentSound: true, sound: 'incident.caf', interruptionLevel: InterruptionLevel.critical)), payload: message.data['incidentId'], // Pass payload for navigation ); } else { const DarwinNotificationDetails iosDetails = DarwinNotificationDetails( presentAlert: true, // Show alert even if in foreground presentBadge: true, presentSound: true, sound: 'general.caf'); var androidDetails = AndroidNotificationDetails( generalChannel.id, generalChannel.name, channelDescription: generalChannel.description, importance: generalChannel.importance, priority: Priority.max, playSound: true, sound: generalChannel.sound, icon: "ic_notification", color: const Color(0xFFCC0000), ); NotificationDetails platformDetails = NotificationDetails(android: androidDetails, iOS: iosDetails); int notificationId = DateTime.now().millisecondsSinceEpoch ~/ 1000; await flutterLocalNotificationsPlugin.show( notificationId, message.data['title'], message.data['body'], platformDetails, ); log("✅ Foreground Notification should be displayed on iOS."); } }); RemoteMessage? initialMessage = await FirebaseMessaging.instance.getInitialMessage(); if (initialMessage != null) { log("getInitialMessage: $initialMessage"); firebaseMessagingBackgroundHandler(initialMessage); } FirebaseMessaging.onMessageOpenedApp.listen((event) { log('onMessageOpenedApp Notification Data: ${event.data}'); if (event.data['type'] == "INCIDENT") { log("Alarm redirection 202-- ${event.data}"); globalNavigatorKey.currentContext?.pushNamed( AppRouteConstants.alarmScreenPageRoute.name, extra: event.data['incidentId']); } }); } static Future<void> requestPermission() async { FirebaseMessaging messaging = FirebaseMessaging.instance; log("FirebaseMessaging TOKEN: ${await messaging.getToken()}"); await messaging.requestPermission( alert: true, announcement: true, badge: true, carPlay: false, criticalAlert: true, provisional: false, sound: true, ); } static Future<void> dismissNotification() async { log("cancelling notification"); await flutterLocalNotificationsPlugin.cancel(0); } } @pragma('vm:entry-point') Future<void> firebaseMessagingBackgroundHandler(RemoteMessage message) async { log("background message ${message.data}"); log("background message notification ${message.notification}"); log("background message notification ${message.notification?.title}"); if (message.data['type'] == "INCIDENT") { //final notification = message.notification; if (message.data.isNotEmpty) { await FirebaseService.flutterLocalNotificationsPlugin.show( 0, message.data['title'], message.data['body'], NotificationDetails( android: AndroidNotificationDetails( FirebaseService.incidentChannel.id, FirebaseService.incidentChannel.name, channelDescription: FirebaseService.incidentChannel.description, importance: Importance.max, priority: Priority.max, playSound: true, visibility: NotificationVisibility.public, autoCancel: false, ongoing: true, sound: const RawResourceAndroidNotificationSound('notification'), color: const Color(0xFFCC0000), ), iOS: const DarwinNotificationDetails( presentAlert: true, presentBadge: true, presentSound: true, sound: 'incident.caf', ), ), payload: message.data['incidentId'], ); } } else { const DarwinNotificationDetails iosDetails = DarwinNotificationDetails( presentAlert: true, // Show alert even if in foreground presentBadge: true, presentSound: true, sound: 'general.caf'); var androidDetails = AndroidNotificationDetails( FirebaseService.generalChannel.id, FirebaseService.generalChannel.name, channelDescription: FirebaseService.generalChannel.description, importance: FirebaseService.generalChannel.importance, priority: Priority.max, playSound: true, sound: FirebaseService.generalChannel.sound, icon: "ic_notification", color: const Color(0xFFCC0000), ); NotificationDetails platformDetails = NotificationDetails(android: androidDetails, iOS: iosDetails); int notificationId = DateTime.now().millisecondsSinceEpoch ~/ 1000; await FirebaseService.flutterLocalNotificationsPlugin.show( notificationId, message.data['title'], message.data['body'], platformDetails, ); log("✅ Foreground Notification should be displayed on iOS."); } }
解决方案
1. 移除重复的初始化代码
init()和initInAPPNotification()中都调用了flutterLocalNotificationsPlugin.initialize(),这会导致通知插件被重复初始化,触发多次通知处理逻辑。
修改init()方法中的初始化逻辑,整合通知点击回调:
// 在init()中替换原初始化代码 final InitializationSettings initializationSettings = InitializationSettings( android: AndroidInitializationSettings('@drawable/ic_notification'), iOS: DarwinInitializationSettings( requestAlertPermission: true, requestBadgePermission: true, requestSoundPermission: true, requestCriticalPermission: true ), ); flutterLocalNotificationsPlugin.initialize( initializationSettings, onDidReceiveNotificationResponse: (details) { log("Alarm redirection 105-- ${details.payload}"); if (details.payload != null && details.payload!.isNotEmpty) { globalNavigatorKey.currentContext?.pushNamed( AppRouteConstants.alarmScreenPageRoute.name, extra: details.payload ); } }, );
然后删除initInAPPNotification()中以下重复代码:
var initializationSettings = const InitializationSettings(...); flutterLocalNotificationsPlugin.initialize(...);
2. 修正getInitialMessage的处理逻辑
进程被杀死后收到通知时,getInitialMessage会获取到消息,此时调用firebaseMessagingBackgroundHandler会重复显示通知(后台消息处理器已自动触发过一次通知显示),仅保留导航逻辑即可:
RemoteMessage? initialMessage = await FirebaseMessaging.instance.getInitialMessage(); if (initialMessage != null) { log("getInitialMessage: $initialMessage"); // 仅处理导航,不重复触发通知显示 if (initialMessage.data['type'] == "INCIDENT") { globalNavigatorKey.currentContext?.pushNamed( AppRouteConstants.alarmScreenPageRoute.name, extra: initialMessage.data['incidentId'] ); } }
3. 确保后台消息处理器仅注册一次
保留initInAPPNotification()中FirebaseMessaging.onBackgroundMessage(firebaseMessagingBackgroundHandler)的注册,同时确保init()中注释掉的注册代码不再启用,避免重复注册。
4. 验证后端payload格式
确保后端推送纯data payload,不包含notification字段,示例格式:
{ "data": { "type": "INCIDENT", "title": "报警通知", "body": "发生紧急事件", "incidentId": "123" }, "to": "FCM_TOKEN" }
内容的提问来源于stack exchange,提问作者Yash Patwari
相关产品推荐
相关产品推荐

