Flutter iOS端FCM通知点击未触发getInitialMessage等回调问题
问题背景
开发Flutter应用时,点击Firebase推送的通知无法触发自定义回调,只会打开应用首页。通知由Node.js后端通过firebase-admin发送,客户端已配置getInitialMessage、onMessageOpenedApp等回调,但均未生效。
后端发送逻辑(Node.js)
/** * Trigger sending a notification */ const sendNotification = async ({ title, body, data, notificationType, topic, sendTo, mediaUrl, }) => { let tokens = []; if (sendTo) { console.log(`sending targeted notitification to: ${sendTo}`); tokens = await getUserFCMTokens(sendTo); } 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', }, }, }; console.log(notification); try { if (tokens.length > 0) { // Send to multiple devices const response = await admin .messaging() .sendEachForMulticast(notification); console.log( `Successfully sent message to ${response.successCount} devices` ); if (response.failureCount > 0) { console.log( `Failed to send message to ${response.failureCount} devices` ); response.responses.forEach((resp, idx) => { if (!resp.success) { console.log( `Failed to send to token at index ${idx}: ${resp.error}` ); } }); } } else if (topic) { // Send to topic const response = await admin.messaging().send(notification); console.log('Successfully sent message:', response); } else { console.log('No valid recipients (tokens or topic) specified'); } } catch (error) { console.error('Error sending notification:', error); } };
通知结构示例
{ data: { group: '6716a89768497667e665546c', media: '', message: '6716a8d868497667e66554a4', notificationType: '3', title: 'title goes here', body: '@zh22: Message goes here ' }, topic: 'group_6716a89768497667e665546c', apns: { headers: { 'apns-priority': '5' }, payload: { aps: [Object] } }, android: { priority: 'high', notification: { click_action: 'FLUTTER_NOTIFICATION_CLICK', priority: 'high', sound: 'default' } } }
客户端配置(Flutter main.dart)
void main() async { WidgetsFlutterBinding.ensureInitialized(); await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); await AppData.initiate(); FirebaseMessaging.onMessage.listen((RemoteMessage message) { print("onMessage: $message"); }); FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) { print("onMessageOpenedApp: $message"); NotificationHandler.handleNotificationOnTap(message); }); FirebaseMessaging.onBackgroundMessage(_onBackgroundMessage); FirebaseMessaging.instance .getInitialMessage() .then(NotificationHandler.handleNotificationOnTap); runApp( ShowCaseWidget(builder: (context) { return const MyApp(); }), ); } @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()); } @pragma('vm:entry-point') void notificationTapBackground(NotificationResponse notificationResponse) { print(notificationResponse); // NotificationHandler.handleNotificationOnTap(); }
客户端日志输出
flutter: onBackgroundMessage: Instance of 'RemoteMessage' flutter: data: {body: @zh22: Message goes here, message: 6716a8d868497667e66554a4, title: @title goes here, group: 6716a89768497667e665546c, notificationType: 3, media: } flutter: remoteMessage: null
关键问题点
- 点击通知后无任何日志输出,
getInitialMessage、onMessageOpenedApp均未触发 NotificationHandler.handleNotificationOnTap()从未执行- iOS环境已配置Push Notification能力,
FirebaseAppDelegateProxyEnabled设为NO
排查方向与解决方案
1. APNS payload配置缺失(iOS核心问题)
当前后端发送的APNS payload仅设置了contentAvailable: true,但没有包含alert字段。对于iOS来说,纯data消息在后台时会触发onBackgroundMessage,但系统不会自动展示通知——你现在是通过本地通知插件手动展示的,但此时点击的是本地通知,而非Firebase系统通知,因此Firebase的onMessageOpenedApp和getInitialMessage不会触发。
修改后端APNS配置,添加alert字段,让系统直接展示通知:
apns: { headers: { 'apns-priority': '5', }, payload: { aps: { contentAvailable: true, alert: { // 新增alert字段 title: title, body: body }, sound: 'default' }, }, },
这样iOS系统会直接展示通知,点击时Firebase的回调才能正常触发。
2. 本地通知初始化时机错误
在_onBackgroundMessage中每次收到消息都重新初始化FlutterLocalNotificationsPlugin,这会覆盖之前的回调配置。应该将初始化移到main函数中,而非后台消息处理函数内:
void main() async { WidgetsFlutterBinding.ensureInitialized(); await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); // 提前初始化本地通知插件 final FlutterLocalNotificationsPlugin localNotificationsPlugin = FlutterLocalNotificationsPlugin(); 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); 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); } }, onDidReceiveBackgroundNotificationResponse: notificationTapBackground // 补充后台点击回调 ); await AppData.initiate(); // ... 其他Firebase回调配置 runApp(...); }
3. 确认FirebaseAppDelegateProxyEnabled配置
虽然你已设置为NO,但需要确保AppDelegate中正确处理了通知回调。如果禁用了代理,需要手动在AppDelegate.swift中添加代码:
import UIKit import Flutter import FirebaseMessaging @UIApplicationMain @objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { FirebaseApp.configure() GeneratedPluginRegistrant.register(with: self) // 注册远程通知 UNUserNotificationCenter.current().delegate = self application.registerForRemoteNotifications() return super.application(application, didFinishLaunchingWithOptions: launchOptions) } // 处理通知点击 override func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { let userInfo = response.notification.request.content.userInfo if let message = Messaging.messaging().appDidReceiveMessage(userInfo) { // 将消息传递给Flutter NotificationCenter.default.post(name: NSNotification.Name("FCMNotificationTap"), object: message) } completionHandler() } }
同时在Flutter中监听这个通知,确保点击事件能传递到你的处理逻辑。
4. 检查getInitialMessage的调用时机
确保getInitialMessage在Firebase初始化完成后调用,当前代码位置是正确的,但需要确认NotificationHandler.handleNotificationOnTap中是否正确处理了null情况——日志中出现remoteMessage: null可能是因为该函数接收了null参数,需要添加判断:
static void handleNotificationOnTap(RemoteMessage? message) { if (message == null) { print("remoteMessage: null"); return; } // 处理跳转逻辑 }
内容的提问来源于stack exchange,提问作者zh24

