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

Flutter应用被杀死时FCM推送通知与Deep Link失效问题

Flutter FCM 杀死状态下通知接收与跳转问题

我在Flutter应用中集成了Firebase Cloud Messaging(FCM)推送通知,并使用app_links包实现深度链接。应用在前台或后台状态下,通知功能正常,点击可打开对应页面;但当应用被杀死(从最近应用列表移除)时,无法接收通知,点击通知也无任何反应。需要解决:

  1. 确保应用在被杀死状态下能接收FCM通知;
  2. 在该状态下点击通知打开应用时,导航到正确页面。

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 端配置

  1. 完善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包,此配置可自动生成,但手动添加能避免兼容性问题。

  1. 处理厂商电池优化
    国产安卓厂商(小米、华为、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();
}
  1. 规范消息类型
    发送FCM时优先使用通知消息(包含notification字段),纯数据消息在应用杀死状态下可能被系统拦截。

iOS 端配置

  1. 添加后台权限
    在Info.plist中添加:
<key>UIBackgroundModes</key>
<array>
    <string>remote-notification</string>
</array>
<key>FirebaseAppDelegateProxyEnabled</key>
<false/>
  1. 验证APNs证书
    确保Firebase控制台已上传有效APNs推送证书,且证书与应用Bundle ID匹配。

  2. 设置消息优先级
    发送FCM时设置apns-priority: 10,确保iOS在应用杀死状态下立即推送通知。


二、解决杀死状态下点击通知跳转问题

核心问题修正

应用杀死状态下启动时,GetX路由可能未初始化完成,直接调用Get.toNamed会失效,需调整跳转逻辑:

  1. 修改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避免重复栈
  }
}
  1. 在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());
}
  1. 修正后台消息处理逻辑
    后台环境中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}');
  // 移除后台环境中的路由跳转调用
}
  1. 验证深度链接配置
    确保app_links包已正确注册深度链接Scheme,Android和iOS的配置文件中需包含对应Scheme声明,保证通知中的URL能被正确解析。

测试建议

  1. 使用Firebase控制台发送测试通知,选择「通知消息」类型并添加url数据字段;
  2. 杀死应用后发送通知,检查系统通知栏是否显示;
  3. 点击通知,验证应用是否启动并跳转到指定页面;
  4. 通过Logcat(Android)或Console(iOS)查看日志,排查错误信息。

内容的提问来源于stack exchange,提问作者Amit Singh

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.13 00:17:04