Calculate Document
在 Flutter 应用中接收消息

在 Flutter 应用中接收消息

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

Related articles

Cut in half: On Cloudmonster Review (2023) 5 Best VPN Browsers in 2024: Enhance Your Online Privacy Article 安卓VPN推荐:2024年最值得使用的五大VPN,快速翻墙享受全球网络 VeloCloud and satellite

收到的消息的处理方式因设备状态而异。如需了解这些情形以及如何将 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 。