Flutter应用被杀死时FCM推送通知与Deep Link失效问题
Flutter FCM 杀死状态下通知接收与跳转问题
我在Flutter应用中集成了Firebase Cloud Messaging(FCM)推送通知,并使用app_links包实现深度链接。应用在前台或后台状态下,通知功能正常,点击可打开对应页面;但当应用被杀死(从最近应用列表移除)时,无法接收通知,点击通知也无任何反应。需要解决:
- 确保应用在被杀死状态下能接收FCM通知;
- 在该状态下点击通知打开应用时,导航到正确页面。
firebase_api.dart
import 'dart:convert'; import 'dart:io'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:get/get.dart'; import 'package:torrins_app/resources/app_helper.dart'; import 'package:torrins_app/resources/app_log.dart'; @pragma('vm:entry-point') Future<void> handleBackgroundMessage(RemoteMessage message) async { AppLog.i('Title: ${message.notification?.title}'); AppLog.i('Body: ${message.notification?.body}'); AppLog.i('Payload: ${message.data}'); FirebaseApi().handleMessage(message); } class FirebaseApi { final _firebaseMessaging = FirebaseMessaging.instance; final _androidChannel = const AndroidNotificationChannel( 'high_importance_channel', 'High Importance Notifications', description: 'This channel is used for important notifications', importance: Importance.high, ); static const _androidIconAsset = '@mipmap/ic_launcher'; final _localNotifications = FlutterLocalNotificationsPlugin(); void handleMessage(RemoteMessage? message) async { if (message == null) return; final notificationData = message.data; /// We will pass Url of the screen to go to the specific screen if (notificationData.containsKey('url')) { final url = notificationData['url']; final uri = Uri.parse(url); final pathSegments = uri.pathSegments; if (pathSegments.isEmpty) return; final routeName = uri.path; final arguments = uri.queryParameters; Get.toNamed(routeName, arguments: arguments); } } 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) async { final notification = message.notification; // Try multiple sources for image URL final imageUrl = message.notification?.android?.imageUrl ?? message.notification?.apple?.imageUrl ?? message.data['image'] ?? // Check data payload message.data['imageUrl']; // Alternative key if (notification == null) return; AppLog.i('Image URL: $imageUrl'); // Download and validate image file String? imagePath; String? iosImagePath; if (imageUrl != null && imageUrl.isNotEmpty) { try { // Determine file extension from URL or default to jpg String fileExtension = 'jpg'; final uri = Uri.tryParse(imageUrl); if (uri != null && uri.path.isNotEmpty) { final pathExtension = uri.path.split('.').last.toLowerCase(); if (['jpg', 'jpeg', 'png', 'gif', 'webp'].contains(pathExtension)) { fileExtension = pathExtension; } } // Download for Android imagePath = await AppHelper.downloadAndSaveFile( imageUrl, 'bigImage.$fileExtension'); // Download for iOS with proper extension iosImagePath = await AppHelper.downloadAndSaveFile( imageUrl, 'iosImage.$fileExtension'); // Verify files exist and have content if (imagePath != null) { final file = File(imagePath); if (!await file.exists() || await file.length() == 0) { AppLog.e( 'Android image file is empty or doesn\'t exist: $imagePath'); imagePath = null; } else { AppLog.i('Android image downloaded successfully: $imagePath'); } } if (iosImagePath != null) { final iosFile = File(iosImagePath); if (!await iosFile.exists() || await iosFile.length() == 0) { AppLog.e( 'iOS image file is empty or doesn\'t exist: $iosImagePath'); iosImagePath = null; } else { AppLog.i('iOS image downloaded successfully: $iosImagePath'); } } } catch (e) { AppLog.e('Error downloading image: $e'); imagePath = null; iosImagePath = null; } } // Create big picture style for Android BigPictureStyleInformation? bigPicture; if (imagePath != null) { bigPicture = BigPictureStyleInformation( FilePathAndroidBitmap(imagePath), contentTitle: notification.title, summaryText: notification.body, hideExpandedLargeIcon: false, // Show large icon when expanded ); } // Create iOS attachments List<DarwinNotificationAttachment>? iosAttachments; if (iosImagePath != null) { try { // Get file extension for proper MIME type detection final extension = iosImagePath.split('.').last.toLowerCase(); String? attachmentType; switch (extension) { case 'jpg': case 'jpeg': attachmentType = 'public.jpeg'; break; case 'png': attachmentType = 'public.png'; break; case 'gif': attachmentType = 'com.compuserve.gif'; break; case 'webp': // WebP might not be supported on older iOS versions attachmentType = null; break; } if (attachmentType != null) { iosAttachments = [ DarwinNotificationAttachment( iosImagePath, identifier: 'image_attachment', ) ]; AppLog.i('iOS attachment created with type: $attachmentType'); } else { AppLog.w('Unsupported image format for iOS: $extension'); } } catch (e) { AppLog.e('Error creating iOS attachment: $e'); iosAttachments = null; } } _localNotifications.show( notification.hashCode, notification.title, notification.body, NotificationDetails( android: AndroidNotificationDetails( _androidChannel.id, _androidChannel.name, channelDescription: _androidChannel.description, priority: Priority.high, importance: Importance.high, icon: _androidIconAsset, styleInformation: bigPicture ?? const DefaultStyleInformation(true, true), // Additional properties that might help largeIcon: imagePath != null ? FilePathAndroidBitmap(imagePath) : null, showWhen: true, when: DateTime.now().millisecondsSinceEpoch, ), iOS: DarwinNotificationDetails( attachments: iosAttachments, presentAlert: true, presentBadge: true, presentBanner: true, presentSound: true, interruptionLevel: InterruptionLevel.active, ), ), payload: jsonEncode(message.toMap()), ); }); } Future initLocalNotifications() async { const android = AndroidInitializationSettings(_androidIconAsset); const iOS = DarwinInitializationSettings( requestAlertPermission: true, requestBadgePermission: true, requestSoundPermission: true, ); const settings = InitializationSettings(android: android, iOS: iOS); await _localNotifications.initialize( settings, onDidReceiveNotificationResponse: (response) { final payload = response.payload; if (payload == null) return; final message = RemoteMessage.fromMap(jsonDecode(payload)); handleMessage(message); }, ); final platform = _localNotifications.resolvePlatformSpecificImplementation< AndroidFlutterLocalNotificationsPlugin>(); await platform?.createNotificationChannel(_androidChannel); } Future<void> initNotifications() async { try { // Request permissions with proper configuration NotificationSettings settings = await _firebaseMessaging.requestPermission( alert: true, announcement: false, badge: true, carPlay: false, criticalAlert: false, provisional: false, sound: true, ); if (settings.authorizationStatus == AuthorizationStatus.authorized) { AppLog.i('User granted permission'); } else if (settings.authorizationStatus == AuthorizationStatus.provisional) { AppLog.i('User granted provisional permission'); } else { AppLog.i('User declined or has not accepted permission'); } final fCMToken = await _firebaseMessaging.getToken(); // Subscribing to a topic for all users to broadcast the common message _firebaseMessaging.subscribeToTopic('all'); AppLog.i('FCM Token: $fCMToken'); print('FCM Token: $fCMToken'); FirebaseMessaging.onBackgroundMessage(handleBackgroundMessage); await initLocalNotifications(); // Initialize local notifications first await initPushNotifications(); } catch (e) { print('FCM Error: $e'); AppLog.e('FCM Error: $e'); } } }
解决方案
一、确保杀死状态下接收FCM通知
Android 端配置
- 完善Manifest配置
在AndroidManifest.xml的<application>标签内添加FCM服务声明:
<service android:name="com.google.firebase.messaging.FirebaseMessagingService" android:exported="false"> <intent-filter> <action android:name="com.google.firebase.MESSAGING_EVENT" /> </intent-filter> </service>
注:如果使用官方firebase_messaging包,此配置可自动生成,但手动添加能避免兼容性问题。
- 处理厂商电池优化
国产安卓厂商(小米、华为、OPPO等)会限制杀死应用的后台进程,需引导用户关闭应用的电池优化,或在代码中请求忽略优化:
import 'package:android_intent_plus/android_intent.dart'; void requestIgnoreBatteryOptimizations() async { final intent = AndroidIntent( action: 'android.settings.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS', data: 'package:com.your.app.package', // 替换为你的应用包名 ); await intent.launch(); }
- 规范消息类型
发送FCM时优先使用通知消息(包含notification字段),纯数据消息在应用杀死状态下可能被系统拦截。
iOS 端配置
- 添加后台权限
在Info.plist中添加:
<key>UIBackgroundModes</key> <array> <string>remote-notification</string> </array> <key>FirebaseAppDelegateProxyEnabled</key> <false/>
验证APNs证书
确保Firebase控制台已上传有效APNs推送证书,且证书与应用Bundle ID匹配。设置消息优先级
发送FCM时设置apns-priority: 10,确保iOS在应用杀死状态下立即推送通知。
二、解决杀死状态下点击通知跳转问题
核心问题修正
应用杀死状态下启动时,GetX路由可能未初始化完成,直接调用Get.toNamed会失效,需调整跳转逻辑:
- 修改handleMessage方法
void handleMessage(RemoteMessage? message) async { if (message == null) return; final notificationData = message.data; if (notificationData.containsKey('url')) { final url = notificationData['url']; final uri = Uri.parse(url); final pathSegments = uri.pathSegments; if (pathSegments.isEmpty) return; final routeName = uri.path; final arguments = uri.queryParameters; // 等待路由初始化完成 await Future.delayed(const Duration(milliseconds: 500)); Get.offNamed(routeName, arguments: arguments); // 使用offNamed避免重复栈 } }
- 在main函数中处理初始消息
确保应用启动时立即检查启动消息:
void main() async { WidgetsFlutterBinding.ensureInitialized(); await Firebase.initializeApp(); await FirebaseApi().initNotifications(); // 处理杀死状态下启动的消息 RemoteMessage? initialMessage = await FirebaseMessaging.instance.getInitialMessage(); if (initialMessage != null) { FirebaseApi().handleMessage(initialMessage); } runApp(const MyApp()); }
- 修正后台消息处理逻辑
后台环境中GetX无法正常工作,后台消息仅记录日志即可:
@pragma('vm:entry-point') Future<void> handleBackgroundMessage(RemoteMessage message) async { AppLog.i('Title: ${message.notification?.title}'); AppLog.i('Body: ${message.notification?.body}'); AppLog.i('Payload: ${message.data}'); // 移除后台环境中的路由跳转调用 }
- 验证深度链接配置
确保app_links包已正确注册深度链接Scheme,Android和iOS的配置文件中需包含对应Scheme声明,保证通知中的URL能被正确解析。
测试建议
- 使用Firebase控制台发送测试通知,选择「通知消息」类型并添加
url数据字段; - 杀死应用后发送通知,检查系统通知栏是否显示;
- 点击通知,验证应用是否启动并跳转到指定页面;
- 通过Logcat(Android)或Console(iOS)查看日志,排查错误信息。
内容的提问来源于stack exchange,提问作者Amit Singh
相关产品推荐
相关产品推荐

