返回

Flutter FCM通知点击不触发?深入解析getInitialMessage问题

IOS

Flutter FCM 通知点击 getInitialMessage() 不触发的问题

在开发 Flutter 应用时,集成 Firebase Cloud Messaging (FCM) 以处理推送通知是常见需求。一个常见的问题是,当用户点击通知时,FirebaseMessaging.instance.getInitialMessage() 或者相关的回调函数没有被调用,导致应用未能正确响应通知点击事件。 这个问题会影响应用的用户体验,需要深入分析其根本原因。

问题分析

getInitialMessage() 的主要用途是获取当应用在 完全终止 的状态下(不是后台运行),由用户点击通知打开时接收到的通知信息。这个函数只会在应用启动时被调用一次 。 如果应用处于后台或前台状态,getInitialMessage() 则不会被触发。同时,onMessageOpenedApp 只在应用程序从后台过渡到前台时,点击通知消息的情况下触发。

出现问题的原因可能有以下几个:

  1. 应用状态 :应用可能没有处于终止状态,导致 getInitialMessage() 未被触发。如果应用只是从后台进入前台,将触发的是 onMessageOpenedApp
  2. FCM 配置问题 :服务器发送的 FCM 通知配置可能存在问题,例如 click_action 或其他必要参数未正确设置,这会直接影响 Flutter 端的回调触发。
  3. onBackgroundMessage 处理逻辑错误 : 如果 onBackgroundMessage 处理不当,尤其是在使用本地通知的情况下,有可能干扰正常的点击事件处理。 错误的 payload 配置也可能会导致无法正确解析。
  4. Android 与 iOS 配置差异 : 不同平台对通知的触发逻辑有些许不同,如果仅仅对一个平台进行开发和测试,容易忽略另一个平台的问题。

解决方案

接下来,我们将针对上述原因提供一系列解决方案:

解决方案一:检查应用状态

  1. 确认 getInitialMessage() 的使用场景 。 确保其只在应用完全关闭状态下启动时才会起作用。
  2. 通过打印日志确认应用启动时机 :在 main() 函数的开始部分和 getInitialMessage()then() 方法中加入打印语句,可以帮助你判断是否执行了 getInitialMessage
void main() async {
    WidgetsFlutterBinding.ensureInitialized();
    print('App Starting!');
    await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
  
    await AppData.initiate();
  
     FirebaseMessaging.instance
        .getInitialMessage()
        .then((message){
        print('Initial Message Received!');
        NotificationHandler.handleNotificationOnTap(message);
    });
   
   runApp(const MyApp());
}

操作步骤:

  1. 修改你的 main.dart,添加上述的打印信息。
  2. 关闭 App, 点击消息重新启动应用。
  3. 查看 console 输出,确保启动时执行了 Initial Message Received 这个 print 语句。

如果启动时这个 print 语句没被执行,那么可能和通知处理的生命周期或者配置有关,需要继续排查。

解决方案二: 检查 FCM 通知配置

  1. 确保 click_action 设置正确 :在 Android 消息体中, click_action 必须设置为 FLUTTER_NOTIFICATION_CLICK,以便应用能够接收点击事件。
  2. 检查 payload 数据 : 检查服务端 payload 是否携带了 data,保证 Flutter 端可以从中提取数据。
  3. 统一Android和IOS 配置 : 注意检查 ios 和 Android 平台是否具有对应的推送配置,比如 IOS 配置 aps 下需要添加 contentAvailable: true
  4. 消息参数匹配 : 确保后端发送的数据键与应用端 message.data 所取出的键值相匹配。

服务端 (Node.js) 代码示例:

const notification = {
     data: {
        ...data,
         notificationType: NOTIFICATION_TYPE[notificationType].toString(),
        title: title,
        body: body,
     },
        ...(topic && { topic: topic }),
       ...(tokens.length > 0 && { tokens }),
        apns: {
          headers: {
            'apns-priority': '5',
             },
           payload: {
             aps: {
              contentAvailable: true,
             },
           },
        },
       android: {
          priority: 'high',
          notification: {
            click_action: 'FLUTTER_NOTIFICATION_CLICK',
             priority: 'high',
              sound: 'default',
           },
       },
 };

操作步骤:

  1. 对比你服务端发送通知的参数,和这里给的参考代码示例,找出参数缺失或不正确的部分。
  2. 修改服务端的 FCM 配置代码,使其 click_action 被正确设置, 并确保 payload data 和 app 端使用的key一致。

解决方案三: 检查 onBackgroundMessage 的处理逻辑

  1. 移除本地通知的代码 : 先临时移除 onBackgroundMessage 中的本地通知插件,观察是否还会出现 getInitialMessage 没有调用的情况。 如果问题解决, 则需要重新审视 onBackgroundMessage 的处理。
  2. 检查 payload 的解析 : onDidReceiveNotificationResponsepayload 参数是否可以解析为一个合适的 RemoteMessage, 特别是在自定义处理了本地推送的情况下, payload 的处理至关重要。 需要注意服务端传递的 message.data 必须为一个 Map 结构才能成功转换。
  3. 安全处理 : 处理通知前要加入安全判断,比如判空等。

修改后(Flutter)的示例代码:

 @pragma("vm:entry-point")
  Future<void> _onBackgroundMessage(RemoteMessage message) async {
    print("onBackgroundMessage: $message");
    print("data: ${message.data}");
     // 注释掉 本地通知的 代码, 避免干扰测试
    /* final NotificationRepository notificationRepository =
          NotificationRepositoryImpl();
    final FlutterLocalNotificationsPlugin localNotificationsPlugin =
        FlutterLocalNotificationsPlugin();
     // show notification
    const AndroidNotificationDetails androidNotificationDetails =
        AndroidNotificationDetails(
            "basecamp_notifications", "Basecamp Notifications",
            importance: Importance.max,
            priority: Priority.high,
            ticker: 'ticker');
    const DarwinNotificationDetails iosNotificationDetails =
        DarwinNotificationDetails(
            presentAlert: true,
            presentSound: true,
            interruptionLevel: InterruptionLevel.active);
    int notificationId = 1;
    const NotificationDetails platformSpecifics = NotificationDetails(
        android: androidNotificationDetails, iOS: iosNotificationDetails);
     await localNotificationsPlugin.initialize(
        const InitializationSettings(
          android: AndroidInitializationSettings("@mipmap/ic_launcher"),
           iOS: DarwinInitializationSettings(),
         ),
        onDidReceiveNotificationResponse: (NotificationResponse details) {
        if (details.payload != null) {
           final data = json.decode(details.payload!);
           final message = RemoteMessage(data: Map<String, String>.from(data));
           NotificationHandler.handleNotificationOnTap(message);
        }
       },
    );
      final title = message.data["title"];
      final body = message.data["body"];
     await localNotificationsPlugin.show(
       notificationId, title, body, platformSpecifics,
      payload: message.data.toString()); */
   }

操作步骤:

  1. 注释掉 onBackgroundMessage 函数中的本地推送代码。
  2. 重新构建应用,然后使用服务端发送通知,点击后观察是否触发了 getInitialMessageonMessageOpenedApp
  3. 重新分析日志输出, 如果此时能够正常处理点击事件,需要检查 payload 结构和服务端消息是否匹配,再逐步加入本地推送的逻辑,避免造成不必要的冲突。

解决方案四:确保 Firebase 设置的正确性

  1. 检查 FirebaseAppDelegateProxyEnabled : 如果是IOS,确保此项配置正确。 FirebaseAppDelegateProxyEnabled设置为NO 表示你在自己的 AppDelegate.swift 文件中手动配置,如果设为 YES 表示交给 Firebase 进行处理。 手动处理需要更多的工作,比如配置远程消息的相关协议回调等等。
// AppDelegate.swift
 import UIKit
import Firebase
 import FirebaseMessaging
 @main
 class AppDelegate: FlutterAppDelegate {
     override func application(
          _ application: UIApplication,
          didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
        ) -> Bool {
            FirebaseApp.configure()
         
           Messaging.messaging().delegate = self
     
       UNUserNotificationCenter.current().delegate = self

            GeneratedPluginRegistrant.register(with: self)

            return super.application(application, didFinishLaunchingWithOptions: launchOptions)
        }
    
    override func application(
        _ application: UIApplication,
           didReceiveRemoteNotification userInfo: [AnyHashable : Any],
        fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void
     ) {
        print("did received remote notification: \(userInfo)")
     completionHandler(UIBackgroundFetchResult.newData)
    }
    }

    extension AppDelegate: MessagingDelegate {
      func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) {
        print("Firebase registration token: \(String(describing: fcmToken))")

     }
    }


   extension AppDelegate: UNUserNotificationCenterDelegate {
   func userNotificationCenter(
          _ center: UNUserNotificationCenter,
          willPresent notification: UNNotification,
         withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
   ) {
         
          let userInfo = notification.request.content.userInfo

       print("Handle push notification, Userinfo is \(userInfo)")

           completionHandler([.banner,.list,.sound])
    }

      func userNotificationCenter(_ center: UNUserNotificationCenter,
           didReceive response: UNNotificationResponse,
            withCompletionHandler completionHandler: @escaping () -> Void) {
             
        let userInfo = response.notification.request.content.userInfo

        print("Handle push notification by click, Userinfo is \(userInfo)")

             completionHandler()
       }

     }

操作步骤:

  1. 检查Xcode项目里的 Info.plist, 确认 FirebaseAppDelegateProxyEnabled 值。 如果 FirebaseAppDelegateProxyEnabled 设置为了YES并且你的 AppDelegate.swift文件没有添加上述回调方法的话,可以改为 NO 然后加上相关的代理方法试试看。 如果已经是 NO , 确保回调函数实现。
  2. 构建运行你的项目, 并查看打印,保证 fcmToken 可以成功获取到。

安全建议

  • 数据验证 :在 handleNotificationOnTap 方法中,务必对接收到的通知数据进行验证,避免应用因恶意数据崩溃或产生安全漏洞。

结语

处理 FCM 通知可能遇到各种挑战, 理解 getInitialMessage() 以及 onMessageOpenedApp 的触发时机是解决问题的关键。 通过逐一检查以上步骤,大多数通知点击事件无法响应的问题都可迎刃而解。 请始终保持代码规范、定期测试,以便及时发现并修复可能出现的问题,从而创建可靠的用户体验。