收到的消息的处理方式因设备状态而异。如需了解这些情形以及如何将 FCM 集成到您自己的应用中,必须先确定设备可能会处于的各种状态:
状态 | 说明 |
---|---|
前台 | 当应用处于打开、查看和使用状态时。 |
后台 | 当应用处于打开状态但在后台运行(最小化)时。通常,当用户按设备上的“主屏幕”按钮、使用应用切换器切换到其他应用或在其他标签页 (Web) 中打开应用时,就会发生这种情况。 |
已终止 | 当设备已锁定或应用未运行时。 |
在应用能够通过 FCM 接收消息载荷之前,必须满足一些前提条件:
- 应用必须至少已打开过一次(以便在 FCM 中注册)。
- 在 iOS 上,如果用户从应用切换器中将应用滑掉,则必须手动重新打开该应用,后台消息才能重新开始工作。
- 在 Android 上,如果用户从设备设置中强制退出应用,则必须手动重新打开该应用,消息才能开始工作。
- 在 Web 上,您必须已使用 Web 推送证书请求令牌(使用
getToken()
)。
请求接收消息的权限
在 iOS、macOS、Web 和 Android 13(或更高版本)上,您必须先获得用户的许可,才能在设备上接收 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 不需要单独的隔离环境),这样一来,即使您的应用未运行,您也可以处理消息。
关于后台消息处理程序,您需要注意以下几点:
- 它不能是匿名函数。
- 它必须是顶级函数(例如,不是需要初始化的类方法)。
- 如果使用的是 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
:
// 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-messaging-compat.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。在 index.html
文件中,通过修改用于引导 Flutter 的 <script>
标记来注册 Worker:
<script src="flutter_bootstrap.js" async>
if ('serviceWorker' in navigator) {
window.addEventListener('load', function () {
navigator.serviceWorker.register('firebase-messaging-sw.js', {
scope: '/firebase-cloud-messaging-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-messaging-sw.js', {
scope: '/firebase-cloud-messaging-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>
接下来,重启您的 Flutter 应用。系统将注册 Service Worker,并通过此文件处理任何后台消息。
处理交互
由于通知是一种可见的提示,因此用户常常会通过点按来与之交互。Android 和 iOS 上的默认行为均是打开应用。也就是说,如果应用当前是终止状态,系统便会启动应用;如果应用在后台运行,系统则会将其转至前台。
根据通知的具体内容,您可能会希望在应用打开时便处理用户与通知的交互。例如,如果系统通过通知发送了新的聊天消息并且用户点按了该消息,那么您可能会希望应用在打开时便同时打开具体对话内容。
firebase-messaging
软件包提供了两种方式来处理此类交互:
getInitialMessage()
:如果应用在打开之前处于终止状态,系统将返回一个包含RemoteMessage
的Future
;并且系统会在用户使用该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
在
resources/values/strings.xml
中指定采用默认语言表示的消息:<string name="notification_title">Hello world</string> <string name="notification_message">This is a message</string>
在
values-language
目录中指定经过翻译处理的消息。例如,在resources/values-fr/strings.xml
中指定采用法语表示的消息:<string name="notification_title">Bonjour le monde</string> <string name="notification_message">C'est un message</string>
在服务器载荷中,不要为本地化消息使用
title
、message
和body
键,而是使用title_loc_key
和body_loc_key
,并将它们设为要显示的消息的name
属性。消息载荷将如下所示:
{ "data": { "title_loc_key": "notification_title", "body_loc_key": "notification_message" } }
iOS
在
Base.lproj/Localizable.strings
中指定采用默认语言表示的消息:"NOTIFICATION_TITLE" = "Hello World"; "NOTIFICATION_MESSAGE" = "This is a message";
在
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 步 - 添加通知服务扩展程序
- 在 Xcode 中,点击 File(文件)> New(新建)> Target…(目标…)
- 模态窗口将显示一系列可能的目标;向下滚动或使用过滤条件选择 Notification Service Extension(通知服务扩展程序)。点击下一步。
- 添加商品名称(使用“ImageNotification”按照本教程进行操作),将语言设置为 Objective-C,然后点击 Finish(完成)。
- 点击 Activate(激活)以启用方案。
第 2 步 - 将目标添加到 Podfile
将新扩展程序添加到 Podfile 中,确保新扩展程序能够访问 Firebase/Messaging
pod:
从导航器中,打开 Podfile:Pod > Podfile
向下滚动到文件底部,然后添加以下内容:
target 'ImageNotification' do use_frameworks! pod 'Firebase/Auth' # Add this line if you are using FirebaseAuth phone authentication pod 'Firebase/Messaging' end
使用
ios
或macos
目录中的pod install
安装或更新 pod。
第 3 步 - 使用扩展程序帮助程序
此时,一切应该仍然正常运行。最后一步是调用扩展程序帮助程序。
从导航器中,选择您的 ImageNotification 扩展程序
打开
NotificationService.m
文件。在文件顶部,在
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。