返回 解决方案三: 检查
Flutter FCM通知点击不触发?深入解析getInitialMessage问题
IOS
2025-01-16 04:37:14
Flutter FCM 通知点击 getInitialMessage()
不触发的问题
在开发 Flutter 应用时,集成 Firebase Cloud Messaging (FCM) 以处理推送通知是常见需求。一个常见的问题是,当用户点击通知时,FirebaseMessaging.instance.getInitialMessage()
或者相关的回调函数没有被调用,导致应用未能正确响应通知点击事件。 这个问题会影响应用的用户体验,需要深入分析其根本原因。
问题分析
getInitialMessage()
的主要用途是获取当应用在 完全终止 的状态下(不是后台运行),由用户点击通知打开时接收到的通知信息。这个函数只会在应用启动时被调用一次 。 如果应用处于后台或前台状态,getInitialMessage()
则不会被触发。同时,onMessageOpenedApp
只在应用程序从后台过渡到前台时,点击通知消息的情况下触发。
出现问题的原因可能有以下几个:
- 应用状态 :应用可能没有处于终止状态,导致
getInitialMessage()
未被触发。如果应用只是从后台进入前台,将触发的是onMessageOpenedApp
。 - FCM 配置问题 :服务器发送的 FCM 通知配置可能存在问题,例如
click_action
或其他必要参数未正确设置,这会直接影响 Flutter 端的回调触发。 onBackgroundMessage
处理逻辑错误 : 如果onBackgroundMessage
处理不当,尤其是在使用本地通知的情况下,有可能干扰正常的点击事件处理。 错误的 payload 配置也可能会导致无法正确解析。- Android 与 iOS 配置差异 : 不同平台对通知的触发逻辑有些许不同,如果仅仅对一个平台进行开发和测试,容易忽略另一个平台的问题。
解决方案
接下来,我们将针对上述原因提供一系列解决方案:
解决方案一:检查应用状态
- 确认
getInitialMessage()
的使用场景 。 确保其只在应用完全关闭状态下启动时才会起作用。 - 通过打印日志确认应用启动时机 :在
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());
}
操作步骤:
- 修改你的
main.dart
,添加上述的打印信息。 - 关闭 App, 点击消息重新启动应用。
- 查看 console 输出,确保启动时执行了
Initial Message Received
这个 print 语句。
如果启动时这个 print
语句没被执行,那么可能和通知处理的生命周期或者配置有关,需要继续排查。
解决方案二: 检查 FCM 通知配置
- 确保
click_action
设置正确 :在 Android 消息体中,click_action
必须设置为FLUTTER_NOTIFICATION_CLICK
,以便应用能够接收点击事件。 - 检查 payload 数据 : 检查服务端 payload 是否携带了
data
,保证 Flutter 端可以从中提取数据。 - 统一Android和IOS 配置 : 注意检查 ios 和 Android 平台是否具有对应的推送配置,比如 IOS 配置
aps
下需要添加contentAvailable: true
。 - 消息参数匹配 : 确保后端发送的数据键与应用端
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',
},
},
};
操作步骤:
- 对比你服务端发送通知的参数,和这里给的参考代码示例,找出参数缺失或不正确的部分。
- 修改服务端的 FCM 配置代码,使其
click_action
被正确设置, 并确保 payload data 和 app 端使用的key一致。
解决方案三: 检查 onBackgroundMessage
的处理逻辑
- 移除本地通知的代码 : 先临时移除
onBackgroundMessage
中的本地通知插件,观察是否还会出现getInitialMessage
没有调用的情况。 如果问题解决, 则需要重新审视onBackgroundMessage
的处理。 - 检查 payload 的解析 :
onDidReceiveNotificationResponse
的payload
参数是否可以解析为一个合适的RemoteMessage
, 特别是在自定义处理了本地推送的情况下, payload 的处理至关重要。 需要注意服务端传递的message.data
必须为一个 Map 结构才能成功转换。 - 安全处理 : 处理通知前要加入安全判断,比如判空等。
修改后(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()); */
}
操作步骤:
- 注释掉
onBackgroundMessage
函数中的本地推送代码。 - 重新构建应用,然后使用服务端发送通知,点击后观察是否触发了
getInitialMessage
和onMessageOpenedApp
。 - 重新分析日志输出, 如果此时能够正常处理点击事件,需要检查 payload 结构和服务端消息是否匹配,再逐步加入本地推送的逻辑,避免造成不必要的冲突。
解决方案四:确保 Firebase 设置的正确性
- 检查
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()
}
}
操作步骤:
- 检查Xcode项目里的
Info.plist
, 确认FirebaseAppDelegateProxyEnabled
值。 如果FirebaseAppDelegateProxyEnabled
设置为了YES
并且你的AppDelegate.swift
文件没有添加上述回调方法的话,可以改为NO
然后加上相关的代理方法试试看。 如果已经是NO
, 确保回调函数实现。 - 构建运行你的项目, 并查看打印,保证
fcmToken
可以成功获取到。
安全建议
- 数据验证 :在
handleNotificationOnTap
方法中,务必对接收到的通知数据进行验证,避免应用因恶意数据崩溃或产生安全漏洞。
结语
处理 FCM 通知可能遇到各种挑战, 理解 getInitialMessage()
以及 onMessageOpenedApp
的触发时机是解决问题的关键。 通过逐一检查以上步骤,大多数通知点击事件无法响应的问题都可迎刃而解。 请始终保持代码规范、定期测试,以便及时发现并修复可能出现的问题,从而创建可靠的用户体验。