Calculate Document
在 Flutter 应用中接收消息

在 Flutter 应用中接收消息

收到的消息的处理方式因设备状态而异。如需了解这些情形以及如何将 FCM 集成到您自己的应用中,必须先确定设备可能会处于的各种状态: 状态 说明 前台 当应用处于打开、查看和使用状态时。 后台 当应用处于打开状态但在后台运行(最小化)时。通常,当用户按设

Related articles

Best Keto Bread Recipe What are the Roots of Cloud Computing? Drawing the Wind: 3 Fun, Easy, and Cute Ways Why Does AstrillVPN Stand Out As The Best VPN For Xfinity? Let’s Find Out What Is Onion Over VPN? Is It Safe to Set Up and Use in 2024?

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

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

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

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

请求接收消息的权限

在iOS、macOS、web 和Android 13(或更高版本)上,您必须先获得用户的许可,才能在设备上接收 FCM 载荷。

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

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

firebasemessage message = firebasemessage.instance;

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

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

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

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

注意:在Android 13 之前的版本中,如果用户未在操作系统设置中停用应用的通知,则 authorizationStatus 会返回authorized。在Android 13 及更高版本中,无法确定用户是否已选择授予/拒绝权限。deny 值表示未确定或已拒绝的权限状态,您需要自行跟踪是否已发出权限请求 。

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

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

消息处理

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

前台消息

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

firebasemessage.onmessage.listen( (remotemessage message) {
  print(' got a message whilst in the foreground ! ') ;
  print('Message data: $ {message.data}') ;

  if (message.notification ! = null) {
    print(' Message is contained also contain 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> _ firebasemessagebackgroundhandler(remotemessage message) async {
  // If you 're go to use other Firebase service in the background , such as Firestore ,
  // make sure you call ` initializeapp ` before using other Firebase service .
  await Firebase.initializeApp( ) ;

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

void main( ) {
  firebasemessage.onbackgroundmessage(_ firebasemessagebackgroundhandler) ;
  runApp(MyApp( )) ;
}

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

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

web

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

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

// Please see this file for the latest firebase-js-sdk version:
// https://github.com/firebase/flutterfire/blob/master/packages/firebase_core/firebase_core_web/lib/src/firebase_sdk_version.dart
importScripts("https://www.gstatic.com/firebasejs/10.7.0/firebase-app-compat.js") ;
importScripts("https://www.gstatic.com/firebasejs/10.7.0/firebase-message-compat.js") ;

firebase.initializeApp( {
  apiKey: " ... ",
  authDomain: " ... ",
  databaseURL: " ... ",
  projectId: " ... ",
  storagebucket: " ... ",
  messageSenderId: " ... ",
  appid: " ... ",
}) ;

const message = firebase.message( ) ;

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

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

接下来,必须注册 Service Worker。在index.html 文件中,通过修改用于引导 Flutter 的<script> 标记来注册 Worker :

<script src="flutter_bootstrap.js" async>
  if ('serviceWorker' in navigator) {
    window.addEventListener('load', function ( ) {
      navigator.serviceWorker.register('firebase-message-sw.js', {
        scope: '/firebase-cloud-message-push-scope',
       }) ;
     }) ;
   }
</script>

如果您仍在使用旧的模板系统,可以通过修改用于引导 Flutter 的<script> 标记来注册 Worker,如下所示:

<html>
<body>
  <script>
      var serviceWorkerVersion = null;
      var scriptLoaded = false;
      function loadMainDartJs( ) {
        if (scriptLoaded) {
          return;
         }
        scriptLoaded = true;
        var scriptTag = document.createElement('script') ;
        scriptTag.src = 'main.dart.js';
        scriptTag.type = 'application/javascript';
        document.body.append(scriptTag) ;
       }

      if ('serviceWorker' in navigator) {
        // Service workers are supported. Use them.
        window.addEventListener('load', function ( ) {
          // Register Firebase Messaging service worker.
          navigator.serviceWorker.register('firebase-message-sw.js', {
            scope: '/firebase-cloud-message-push-scope',
           }) ;

          // 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;

          navigator.serviceWorker.register(serviceWorkerUrl).then( (reg) => {
            function waitForActivation(serviceWorker) {
              serviceWorker.addEventListener('statechange', ( ) => {
                if (serviceWorker.state == 'activated') {
                  console.log('Installed new service worker.') ;
                  loadMainDartJs( ) ;
                 }
               }) ;
             }
            if (!reg.active && (reg.installing || reg.waiting)) {
              // No active web worker and we have installed or are installing
              // one for the first time. Simply wait for it to activate.
              waitForActivation(reg.installing ?? reg.waiting) ;
             } else if (!reg.active.scriptURL.endsWith(serviceWorkerVersion)) {
              // When the app updates the serviceWorkerVersion changes, so we
              // need to ask the service worker to update.
              console.log('New service worker available.') ;
              reg.update( ) ;
              waitForActivation(reg.installing) ;
             } else {
              // Existing service worker is still good.
              console.log('Loading app from service worker.') ;
              loadMainDartJs( ) ;
             }
           }) ;

          // If service worker doesn't succeed in a reasonable amount of time,
          // fallback to plaint <script> tag.
          setTimeout( ( ) => {
            if (!scriptLoaded) {
              console.warn(
                'Failed to load app from service worker. Falling back to plain <script> tag.'
              ) ;
              loadMainDartJs( ) ;
             }
           }, 4000) ;
         }) ;
       } else {
        // Service workers not supported. Just drop the <script> tag.
        loadMainDartJs( ) ;
       }
  </script>
</body>

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

处理交互

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

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

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

  • 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 firebasemessage.instance.getInitialMessage( ) ;

    // If the message is contains also contain a datum 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
    firebasemessage.onmessageOpenedApp.listen(_handleMessage) ;
  }

  void _handleMessage(remotemessage message) {
    if (message.data[' type '] == ' chat ') {
      Navigator.pushname(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- 目录中指定经过翻译处理的消息。例如,在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.string 中指定采用默认语言表示的消息 :

    "NOTIFICATION_TITLE" = "Hello World";
    "NOTIFICATION_MESSAGE" = "This is a message";
    

  2. .lproj 目录中指定经过翻译处理的消息。例如,在fr.lproj/localizable.string 中指定采用法语表示的消息:

    " 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 firebasemessage.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/message/sw';
...

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

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

在iOS 的通知中显示图片

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

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

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

  1. 在Xcode 中,点击 File(文件)> New(新建)> Target…(目标…)
  2. 模态窗口将显示一系列可能的目标;向下滚动或使用过滤条件选择 Notification Service extension(通知服务扩展程序)。点击下一步 。
  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 is install install 安装或更新 pod。

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

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

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

  2. 打开 NotificationService.m 文件。

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

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

    #import "NotificationService.h"
    #import "firebasemessage.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 {
        // call just before the extension will be terminate 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 文档。请注意,设备强制执行的图片大小上限为 300 KB 。