You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 03:25:55