在 Flutter 应用中接收消息

收到的消息的处理方式因设备状态而异。如需了解这些情形以及如何将 FCM 集成到您自己的应用中,必须先确定设备可能会处于的各种状态:

状态 说明
前台 当应用处于打开、查看和使用状态时。
后台 当应用处于打开状态但在后台运行(最小化)时。通常,当用户按设备上的“主屏幕”按钮、使用应用切换器切换到其他应用或在其他标签页 (Web) 中打开应用时,就会发生这种情况。
已终止 当设备已锁定或应用未运行时。

在应用能够通过 FCM 接收消息载荷之前,必须满足一些前提条件:

  • 应用必须至少已打开过一次(以便在 FCM 中注册)。
  • 在 iOS 上,如果用户从应用切换器中将应用滑掉,则必须手动重新打开该应用,后台消息才能重新开始工作。
  • 在 Android 上,如果用户从设备设置中强制退出应用,则必须手动重新打开该应用,消息才能开始工作。
  • 在 Web 上,您必须已使用 Web 推送证书请求令牌(使用 getToken())。

请求权限以接收消息(Apple 和 Web)

在 iOS、macOS 和 Web 上,您必须先获得用户的许可,才能在设备上接收 FCM 载荷。

firebase_messaging 软件包提供了一个简单的 API,用于通过 requestPermission 方法请求权限。此 API 会接受多个命名参数,这些参数定义了您要请求的权限类型,例如包含通知载荷的消息功能是否可以触发声音或通过 Siri 读出消息。默认情况下,该方法会请求合理的默认权限。参考 API 提供了有关每项权限用途的完整文档。

如需开始请求权限,请从您的应用调用该方法(在 iOS 上,系统将显示原生模态;在 Web 上,系统将触发浏览器的原生 API 流程):

FirebaseMessaging messaging = FirebaseMessaging.instance;

NotificationSettings settings = await messaging.requestPermission(
  alert: true,
  announcement: false,
  badge: true,
  carPlay: false,
  criticalAlert: false,
  provisional: false,
  sound: true,
);

print('User granted permission: ${settings.authorizationStatus}');

从请求返回的 NotificationSettings 对象的 authorizationStatus 属性可用于确定用户的总体决定:

  • authorized:用户授予了权限。
  • denied:用户拒绝了权限。
  • notDetermined:用户尚未选择是否要授予权限。
  • provisional:用户授予了临时权限。

NotificationSettings 中的其他属性会返回关于当前设备是已启用、已停用还是不支持特定权限的信息。

获得权限并了解不同类型的设备状态后,您的应用现在便可以开始处理传入的 FCM 载荷了。

消息处理

根据应用的当前状态,不同消息类型的传入载荷需要不同的实现来处理:

前台消息

如需在应用在前台运行的情况下处理消息,请监听 onMessage 流。

FirebaseMessaging.onMessage.listen((RemoteMessage message) {
  print('Got a message whilst in the foreground!');
  print('Message data: ${message.data}');

  if (message.notification != null) {
    print('Message also contained a notification: ${message.notification}');
  }
});

该流包含一个 RemoteMessage,其中详细说明了有关载荷的各种信息,例如载荷的来源、唯一 ID、发送时间、载荷是否包含通知等等。由于消息是应用在前台运行时检索的,因此您可以直接访问 Flutter 应用的状态和上下文。

前台消息和通知消息

默认情况下,应用在前台运行时送达的通知消息不会在 Android 和 iOS 上显示可见的通知。不过,您可以替换此行为:

  • 在 Android 上,您必须创建“高优先级”通知渠道。
  • 在 iOS 上,您可以更新应用的呈现选项。

后台消息

在原生(Android 和 Apple)和基于 Web 的平台上,处理后台消息的过程有所不同。

Apple 平台和 Android

通过注册 onBackgroundMessage 处理程序来处理后台消息。收到消息后,系统会生成一个隔离环境(仅限 Android,iOS/macOS 不需要单独的隔离环境),这样一来,即使您的应用未运行,您也可以处理消息。

关于后台消息处理程序,您需要注意以下几点:

  1. 它不能是匿名函数。
  2. 它必须是顶级函数(例如,不是需要初始化的类方法)。
  3. 如果使用的是 Flutter 3.3.0 版或更高版本,则必须紧接函数声明之前用 @pragma('vm:entry-point') 标注消息处理程序(否则,对于发布模式,可能会在摇树优化期间将其移除)。
@pragma('vm:entry-point')
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
  // If you're going to use other Firebase services in the background, such as Firestore,
  // make sure you call `initializeApp` before using other Firebase services.
  await Firebase.initializeApp();

  print("Handling a background message: ${message.messageId}");
}

void main() {
  FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
  runApp(MyApp());
}

由于处理程序在应用上下文之外的自有隔离环境中运行,因此无法更新应用状态或执行任何影响逻辑的界面。但是,您可以执行 HTTP 请求之类的逻辑、执行 IO 操作(例如更新本地存储空间)、与其他插件通信,等等。

我们还建议您尽快完成自己的逻辑。运行耗时较长的密集型任务会影响设备性能,并且可能导致操作系统终止进程。如果任务运行时间超过 30 秒,设备可能就会自动终止进程。

Web

在 Web 上,编写一个在后台运行的 JavaScript Service Worker。使用 Service Worker 处理后台消息。

首先,在 web 目录中创建一个新文件,并将其命名为 firebase-messaging-sw.js

importScripts("https://www.gstatic.com/firebasejs/8.10.0/firebase-app.js");
importScripts("https://www.gstatic.com/firebasejs/8.10.0/firebase-messaging.js");

firebase.initializeApp({
  apiKey: "...",
  authDomain: "...",
  databaseURL: "...",
  projectId: "...",
  storageBucket: "...",
  messagingSenderId: "...",
  appId: "...",
});

const messaging = firebase.messaging();

// Optional:
messaging.onBackgroundMessage((message) => {
  console.log("onBackgroundMessage", message);
});

该文件必须导入应用和消息传递 SDK,初始化 Firebase 并公开 messaging 变量。

接下来,必须注册 Service Worker。在入口文件中,注册 Service Worker(在 main.dart.js 文件加载之后):

<html>
<body>
  ...
  <script src="main.dart.js" type="application/javascript"></script>
  <script>
       if ('serviceWorker' in navigator) {
          // Service workers are supported. Use them.
          window.addEventListener('load', function () {
            // ADD THIS LINE
            navigator.serviceWorker.register('/firebase-messaging-sw.js');

            // Wait for registration to finish before dropping the <script> tag.
            // Otherwise, the browser will load the script multiple times,
            // potentially different versions.
            var serviceWorkerUrl = 'flutter_service_worker.js?v=' + serviceWorkerVersion;

            //  ...
          });
      }
  </script>

接下来,重启您的 Flutter 应用。系统将注册 Service Worker,并通过此文件处理任何后台消息。

处理交互

由于通知是一种可见的提示,因此用户常常会通过点按来与之交互。Android 和 iOS 上的默认行为均是打开应用。也就是说,如果应用当前是终止状态,系统便会启动应用;如果应用在后台运行,系统则会将其转至前台。

根据通知的具体内容,您可能会希望在应用打开时便处理用户与通知的交互。例如,如果系统通过通知发送了新的聊天消息并且用户点按了该消息,那么您可能会希望应用在打开时便同时打开具体对话内容。

firebase-messaging 软件包提供了两种方式来处理此类交互:

  • getInitialMessage():如果应用在打开之前处于终止状态,系统将返回一个包含 RemoteMessageFuture;并且系统会在用户使用该 RemoteMessage 之后将其移除。
  • onMessageOpenedApp:如果应用在打开之前处于后台状态,则系统会通过一个 Stream 来发布 RemoteMessage

建议对这两种情况都予以处理,以确保为用户提供顺畅的用户体验。以下代码示例简单展示了实现上述操作的方法:

class Application extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => _Application();
}

class _Application extends State<Application> {
  // It is assumed that all messages contain a data field with the key 'type'
  Future<void> setupInteractedMessage() async {
    // Get any messages which caused the application to open from
    // a terminated state.
    RemoteMessage? initialMessage =
        await FirebaseMessaging.instance.getInitialMessage();

    // If the message also contains a data property with a "type" of "chat",
    // navigate to a chat screen
    if (initialMessage != null) {
      _handleMessage(initialMessage);
    }

    // Also handle any interaction when the app is in the background via a
    // Stream listener
    FirebaseMessaging.onMessageOpenedApp.listen(_handleMessage);
  }

  void _handleMessage(RemoteMessage message) {
    if (message.data['type'] == 'chat') {
      Navigator.pushNamed(context, '/chat',
        arguments: ChatArguments(message),
      );
    }
  }

  @override
  void initState() {
    super.initState();

    // Run code required to handle interacted messages in an async function
    // as initState() must not be async
    setupInteractedMessage();
  }

  @override
  Widget build(BuildContext context) {
    return Text("...");
  }
}

具体使用哪一种交互处理方式取决于您的应用设置。上面的示例是对 StatefulWidget 用例的一个基本展示。

将消息内容本地化

您可以通过两种不同的方式发送经过本地化的字符串:

  • 将每位用户的偏好语言存储在您的服务器中,并向用户发送针对相应语言进行自定义的通知
  • 在应用中嵌入经过本地化的字符串,并利用操作系统的原生语言区域设置

下面介绍如何使用第二种方法:

Android

  1. resources/values/strings.xml 中指定采用默认语言表示的消息:

    <string name="notification_title">Hello world</string>
    <string name="notification_message">This is a message</string>
    
  2. values-language 目录中指定经过翻译处理的消息。例如,在 resources/values-fr/strings.xml 中指定采用法语表示的消息:

    <string name="notification_title">Bonjour le monde</string>
    <string name="notification_message">C'est un message</string>
    
  3. 在服务器载荷中,不要为本地化消息使用 titlemessagebody 键,而是使用 title_loc_keybody_loc_key,并将它们设为要显示的消息的 name 属性。

    消息载荷将如下所示:

    {
      "data": {
        "title_loc_key": "notification_title",
        "body_loc_key": "notification_message"
      }
    }
    

iOS

  1. Base.lproj/Localizable.strings 中指定采用默认语言表示的消息:

    "NOTIFICATION_TITLE" = "Hello World";
    "NOTIFICATION_MESSAGE" = "This is a message";
    
  2. language.lproj 目录中指定经过翻译处理的消息。例如,在 fr.lproj/Localizable.strings 中指定采用法语表示的消息:

    "NOTIFICATION_TITLE" = "Bonjour le monde";
    "NOTIFICATION_MESSAGE" = "C'est un message";
    

    消息载荷将如下所示:

    {
      "data": {
        "title_loc_key": "NOTIFICATION_TITLE",
        "body_loc_key": "NOTIFICATION_MESSAGE"
      }
    }
    

启用消息传送数据导出功能

您可以将消息数据导出至 BigQuery 以便进一步分析。借助 BigQuery,您可以使用 BigQuery SQL 来分析数据,将数据导出至其他云服务商,或将该数据用于自定义机器学习模型。导出到 BigQuery 的操作包括消息的所有可用数据,无论消息类型为何,也无论消息通过 API 发送还是通过 Notifications Composer 发送。

如需启用导出功能,请先点击此链接按照相关步骤操作,然后按照以下说明操作:

Android

您可以使用以下代码:

await FirebaseMessaging.instance.setDeliveryMetricsExportToBigQuery(true);

iOS

对于 iOS,您需要将 AppDelegate.m 更改为下面的内容。

#import "AppDelegate.h"
#import "GeneratedPluginRegistrant.h"
#import <Firebase/Firebase.h>

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application
    didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
  [GeneratedPluginRegistrant registerWithRegistry:self];
  // Override point for customization after application launch.
  return [super application:application didFinishLaunchingWithOptions:launchOptions];
}

- (void)application:(UIApplication *)application
    didReceiveRemoteNotification:(NSDictionary *)userInfo
          fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {
  [[FIRMessaging extensionHelper] exportDeliveryMetricsToBigQueryWithMessageInfo:userInfo];
}

@end

Web

对于 Web,您需要更改 Service Worker,才能使用 v9 版本的 SDK。v9 版本需要捆绑使用,因此您需要先使用捆绑器(如 esbuild)让 Service Worker 能够正常工作。请参阅示例应用,了解如何执行此操作。

迁移到 v9 SDK 后,您可以使用以下代码:

import {
  experimentalSetDeliveryMetricsExportedToBigQueryEnabled,
  getMessaging,
} from 'firebase/messaging/sw';
...

const messaging = getMessaging(app);
experimentalSetDeliveryMetricsExportedToBigQueryEnabled(messaging, true);

不要忘记运行 yarn build,以将新版 Service Worker 导出到 web 文件夹。

在 iOS 的通知中显示图片

在 Apple 设备上,为了让传入的 FCM 通知显示来自 FCM 载荷的图片,您必须添加额外的通知服务扩展程序,并将您的应用配置为使用该扩展程序。

如果您使用的是 Firebase 手机身份验证,则必须将 Firebase Auth pod 添加到您的 Podfile 中。

第 1 步 - 添加通知服务扩展程序

  1. 在 Xcode 中,点击 File(文件)> New(新建)> Target…(目标…)
  2. 模态窗口将显示一系列可能的目标;向下滚动或使用过滤条件选择 Notification Service Extension(通知服务扩展程序)。点击 Next(下一步)。
  3. 添加商品名称(使用“ImageNotification”按照本教程进行操作),将语言设置为 Objective-C,然后点击 Finish(完成)。
  4. 点击 Activate(激活)以启用方案。

第 2 步 - 将目标添加到 Podfile

将新扩展程序添加到 Podfile 中,确保新扩展程序能够访问 Firebase/Messaging pod:

  1. 从导航器中,打开 Podfile:Pod > Podfile

  2. 向下滚动到文件底部,然后添加以下内容:

    target 'ImageNotification' do
      use_frameworks!
      pod 'Firebase/Auth' # Add this line if you are using FirebaseAuth phone authentication
      pod 'Firebase/Messaging'
    end
    
  3. 使用 iosmacos 目录中的 pod install 安装或更新 pod。

第 3 步 - 使用扩展程序帮助程序

此时,一切应该仍然正常运行。最后一步是调用扩展程序帮助程序。

  1. 从导航器中,选择您的 ImageNotification 扩展程序

  2. 打开 NotificationService.m 文件。

  3. 在文件顶部,在 NotificationService.h 之后导入 FirebaseMessaging.h,如下所示。

    NotificationService.m 的内容替换为以下内容:

    #import "NotificationService.h"
    #import "FirebaseMessaging.h"
    #import "FirebaseAuth.h" // Add this line if you are using FirebaseAuth phone authentication
    #import <UIKit/UIKit.h> // Add this line if you are using FirebaseAuth phone authentication
    
    @interface NotificationService ()
    
    @property (nonatomic, strong) void (^contentHandler)(UNNotificationContent *contentToDeliver);
    @property (nonatomic, strong) UNMutableNotificationContent *bestAttemptContent;
    
    @end
    
    @implementation NotificationService
    
    /* Uncomment this if you are using Firebase Auth
    - (BOOL)application:(UIApplication *)app
                openURL:(NSURL *)url
                options:(NSDictionary<UIApplicationOpenURLOptionsKey, id> *)options {
      if ([[FIRAuth auth] canHandleURL:url]) {
        return YES;
      }
      return NO;
    }
    
    - (void)scene:(UIScene *)scene openURLContexts:(NSSet<UIOpenURLContext *> *)URLContexts {
      for (UIOpenURLContext *urlContext in URLContexts) {
        [FIRAuth.auth canHandleURL:urlContext.URL];
      }
    }
    */
    
    - (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler {
        self.contentHandler = contentHandler;
        self.bestAttemptContent = [request.content mutableCopy];
    
        // Modify the notification content here...
        [[FIRMessaging extensionHelper] populateNotificationContent:self.bestAttemptContent withContentHandler:contentHandler];
    }
    
    - (void)serviceExtensionTimeWillExpire {
        // Called just before the extension will be terminated by the system.
        // Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used.
        self.contentHandler(self.bestAttemptContent);
    }
    
    @end
    

第 4 步 - 将图片添加到载荷

现在,您可以在通知载荷中添加图片。请参阅关于如何构建发送请求的 iOS 文档。请注意,设备强制执行的图片大小上限为 300KB。