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

Flutter中Firebase离线推送通知联网后仅收一条的问题求助

解决Flutter Firebase离线推送通知只显示一条的问题

问题核心原因

Firebase Cloud Messaging(FCM)的通知消息(携带notification字段)在设备离线时,FCM服务器会自动合并同类型通知,仅保留最新一条;而数据消息(仅含data字段)不会被合并,离线时FCM会缓存所有消息,待设备联网后逐条推送给客户端,但这类消息需要你手动处理通知展示逻辑。

解决方案

要实现类似WhatsApp的全量离线通知接收效果,需采用纯Data消息推送,并在客户端手动处理所有场景下的通知展示:

1. 后端推送消息格式

发送仅含data字段的消息,将标题、内容、唯一标识等信息放入data中:

{
  "to": "设备FCM_TOKEN",
  "data": {
    "title": "好友发来消息",
    "body": "今晚一起吃饭吗?",
    "messageId": "msg_20240520_001", // 每条消息的唯一ID,用于生成通知ID
    "customData": "自定义业务数据"
  }
}

2. 客户端代码修改

修改你的Firebase通知处理逻辑,确保在前台、后台、应用杀死状态下都能正确展示每条离线推送的通知:

修改后的完整代码

import 'dart:convert';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';

// 后台消息处理:纯Data消息在后台/杀死状态下会触发此方法
Future handleBackgroundMessage(RemoteMessage message) async {
  // 从data字段提取通知内容
  final title = message.data['title'] ?? '默认标题';
  final body = message.data['body'] ?? '默认内容';
  final messageId = message.data['messageId'] ?? DateTime.now().millisecondsSinceEpoch.toString();

  // 初始化本地通知插件(后台环境需重新初始化)
  final localNotification = FlutterLocalNotificationsPlugin();
  const androidInit = AndroidInitializationSettings("@drawable/ic_launcher");
  const iosInit = DarwinInitializationSettings();
  await localNotification.initialize(
    InitializationSettings(android: androidInit, iOS: iosInit),
  );

  // 创建通知渠道(仅Android需要)
  final androidChannel = const AndroidNotificationChannel(
    'high_importance_channel',
    'High Importance Notifications',
    description: "用于展示所有重要通知",
    importance: Importance.high, // 必须设为high才能在后台弹出通知
  );
  final androidPlugin = localNotification.resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>();
  await androidPlugin?.createNotificationChannel(androidChannel);

  // 显示本地通知
  await localNotification.show(
    int.parse(messageId), // 用唯一messageId作为通知ID,避免覆盖
    title,
    body,
    NotificationDetails(
      android: AndroidNotificationDetails(
        androidChannel.id,
        androidChannel.name,
        channelDescription: androidChannel.description,
        icon: "@drawable/ic_launcher",
      ),
    ),
    payload: jsonEncode(message.toMap()),
  );
}

class FirebaseApi {
  final _firebaseMessaging = FirebaseMessaging.instance;
  final _androidChannel = const AndroidNotificationChannel(
    'high_importance_channel',
    'High Importance Notifications',
    description: "用于展示所有重要通知",
    importance: Importance.high,
  );
  final _localNotification = FlutterLocalNotificationsPlugin();

  void handleMessage(RemoteMessage? message) {
    if (message == null) return;
    // 处理通知点击后的逻辑
    print("点击通知,数据:${message.data}");
  }

  Future initLocalNotifications() async {
    const iOS = DarwinInitializationSettings();
    const android = AndroidInitializationSettings("@drawable/ic_launcher");
    const settings = InitializationSettings(android: android, iOS: iOS);

    await _localNotification.initialize(settings,
        onDidReceiveNotificationResponse: (response) {
      final message = RemoteMessage.fromMap(jsonDecode(response.payload!));
      handleMessage(message);
    });

    final platform = _localNotification.resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>();
    await platform?.createNotificationChannel(_androidChannel);
  }

  Future initPushNotifications() async {
    await FirebaseMessaging.instance
        .setForegroundNotificationPresentationOptions(
            alert: true, badge: true, sound: true);

    // 处理应用启动时的通知
    FirebaseMessaging.instance.getInitialMessage().then(handleMessage);
    // 处理后台点击通知
    FirebaseMessaging.onMessageOpenedApp.listen(handleMessage);
    // 注册后台消息处理
    FirebaseMessaging.onBackgroundMessage(handleBackgroundMessage);

    // 前台接收消息时展示通知
    FirebaseMessaging.onMessage.listen(
      (message) {
        final title = message.data['title'] ?? '默认标题';
        final body = message.data['body'] ?? '默认内容';
        final messageId = message.data['messageId'] ?? DateTime.now().millisecondsSinceEpoch.toString();

        _localNotification.show(
          int.parse(messageId), // 唯一ID确保每条通知独立显示
          title,
          body,
          NotificationDetails(
            android: AndroidNotificationDetails(
                _androidChannel.id, _androidChannel.name,
                channelDescription: _androidChannel.description,
                icon: "@drawable/ic_launcher"),
          ),
          payload: jsonEncode(message.toMap()),
        );
      },
    );
  }

  Future<String?> initNotification() async {
    await _firebaseMessaging.requestPermission(
      alert: true,
      badge: true,
      sound: true,
      provisional: false,
    );
    String? fcmToken = await _firebaseMessaging.getToken();
    print("FCM Token: $fcmToken");
    await initLocalNotifications();
    await initPushNotifications();
    return fcmToken;
  }
}

关键修改点说明

  • 通知ID唯一性:使用消息自带的messageId(或时间戳)作为通知ID,避免多条通知互相覆盖
  • 后台消息处理:在handleBackgroundMessage中手动初始化本地通知并展示,确保应用杀死/后台状态下也能接收并显示离线推送
  • 纯Data消息处理:所有消息都从data字段提取标题和内容,不再依赖FCM的notification字段
  • Android通知渠道优先级:将渠道importance设为Importance.high,确保通知能在后台正常弹出

额外配置

  • iOS:在Info.plist中开启后台远程通知权限:
    <key>UIBackgroundModes</key>
    <array>
      <string>remote-notification</string>
    </array>
    
  • Android:确保AndroidManifest.xml中已添加FCM相关配置,且通知渠道创建正确

内容的提问来源于stack exchange,提问作者Berlato Matteo

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.22 22:55:54