本指南說明如何在行動和網頁用戶端應用程式中設定 Firebase Cloud Messaging,以便穩定接收訊息。
在 Flutter 應用程式中接收訊息
系統會根據裝置狀態,以不同方式處理來信。如要瞭解這些情境,以及如何將 FCM 整合至自己的應用程式,請先瞭解裝置可能處於的各種狀態:
| 狀態 | 說明 | 
|---|---|
| 前景 | 應用程式開啟、顯示在畫面上且正在使用中。 | 
| 背景 | 應用程式已開啟,但處於背景執行狀態 (已縮小)。使用者按下裝置的「主畫面」按鈕、使用應用程式切換器切換至其他應用程式,或在不同分頁中開啟應用程式 (網頁) 時,通常會發生這種情況。 | 
| 已終止 | 裝置處於鎖定狀態或應用程式未執行時。 | 
應用程式必須符合下列先決條件,才能使用 FCM 接收訊息酬載:
- 應用程式必須至少開啟過一次 (才能向 FCM 註冊)。
- 在 iOS 上,如果使用者從應用程式切換器滑動關閉應用程式,就必須手動重新開啟,背景訊息才會再次運作。
- 在 Android 裝置上,如果使用者透過裝置設定強制停止應用程式,必須手動重新開啟應用程式,訊息功能才會開始運作。
- 在網頁上,您必須使用網頁推送憑證要求權杖 (使用 getToken())。
要求接收訊息的權限
在 iOS、macOS、網頁和 Android 13 (或更新版本) 上,您必須先徵求使用者同意,裝置才能接收 FCM 酬載。
firebase_messaging 套件提供 API,可使用 requestPermission 方法要求權限。這個 API 接受多個具名引數,用於定義您要要求的權限類型,例如含有通知酬載的訊息是否可以觸發音效,或是使用 Siri 朗讀訊息。根據預設,這個方法會要求合理的預設權限。參考 API 提供完整說明文件,介紹各項權限的用途。
如要開始使用,請從應用程式呼叫方法 (系統會在 iOS 上顯示內建模式,並在網頁上觸發瀏覽器的 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 和網頁平台處理背景訊息的程序有所不同。
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());
}
由於處理常式會在應用程式環境外的獨立隔離區中執行,因此無法更新應用程式狀態或執行任何會影響 UI 的邏輯。不過,您可以執行 HTTP 要求等邏輯、執行 IO 作業 (例如更新本機儲存空間)、與其他外掛程式通訊等。
建議您盡快完成邏輯。執行長時間的密集工作會影響裝置效能,且可能導致 OS 終止程序。如果工作執行時間超過 30 秒,裝置可能會自動終止程序。
網頁
在網路上,編寫在背景執行的 JavaScript Service Worker。使用 Service Worker 處理背景訊息。
首先,請在 web 目錄中建立新檔案,並命名為 firebase-messaging-sw.js:
// See this file for the latest firebase-js-sdk version:
// https://github.com/firebase/flutterfire/blob/main/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);
});
檔案必須匯入應用程式和 Messaging SDK,初始化 Firebase 並公開 messaging 變數。
接著,工作人員必須註冊。在 index.html 檔案中,修改啟動 Flutter 的 <script> 標記,藉此註冊背景工作人員:
<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> 標記,註冊工作人員,如下所示:
<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 應用程式。系統會註冊該工作人員,並使用這個檔案處理任何背景訊息。
處理互動
由於通知是可見的提示,使用者通常會與通知互動 (按下)。在 Android 和 iOS 裝置上,預設行為都是開啟應用程式。如果應用程式已終止,系統會啟動應用程式;如果應用程式在背景執行,系統會將其移至前景。
視通知內容而定,您可能想在應用程式開啟時處理使用者的互動。舉例來說,如果使用者透過通知傳送新的即時通訊訊息,並按下通知,您可能會想在應用程式開啟時開啟特定對話。
firebase-messaging 套件提供兩種處理這類互動的方式:
- getInitialMessage():如果應用程式是從終止狀態開啟,系統會傳回包含- RemoteMessage的- Future。- RemoteMessage一經使用就會移除。
- onMessageOpenedApp:應用程式從背景狀態開啟時,會發布- RemoteMessage的- Stream。
建議您處理這兩種情況,確保使用者享有流暢的使用體驗。以下程式碼範例說明如何達成這個目標:
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 using 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_loc_key和- body_loc_key鍵 (而非- title、- message和- body鍵) 傳送本地化訊息,並將這些鍵設為要顯示訊息的- name屬性。- 訊息酬載會如下所示: - { "android": { "notification": { "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";- 訊息酬載會如下所示: - { "apns": { "payload": { "alert": { "title-loc-key": "NOTIFICATION_TITLE", "loc-key": "NOTIFICATION_MESSAGE" } } } }
啟用訊息傳送資料匯出功能
您可以將訊息資料匯出至 BigQuery,以進行進一步分析。您可以使用 BigQuery SQL 分析資料、將資料匯出至其他雲端供應商,或將資料用於自訂 ML 模型。匯出至 BigQuery 的資料包含所有可用的訊息資料,無論訊息類型為何,或訊息是透過 API 還是通知撰寫工具傳送。
如要啟用匯出功能,請先按照「BigQuery 資料匯出」一文中的步驟操作。在應用程式例項層級以程式輔助方式啟用這項功能,可讓您要求使用者授權分析訊息傳送資料 (建議)。請按照下列操作說明,以程式輔助方式啟用匯出功能:
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
網頁
如果是網頁,您必須變更服務工作人員,才能使用 SDK 的第 9 版。
您必須將 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...」
- 模式會顯示可能的目標清單,請捲動或使用篩選器選取「通知服務擴充功能」。點選「下一步」。
- 新增產品名稱 (請使用「ImageNotification」來跟著本教學課程操作),選取 Swift或Objective-C,然後按一下「完成」。
- 按一下「啟用」啟用該方案。
步驟 2 - 將目標新增至 Podfile
Swift
請將新擴充功能加入 Runner 目標,確保擴充功能可存取 FirebaseMessaging Swift 套件:
- 在「Navigator」中新增 Firebase Apple 平台 SDK:依序點選「File」>「Add Package Dependencies...」 
- 搜尋或輸入套件網址: - none https://github.com/firebase/firebase-ios-sdk
- 新增至專案 - Runner:新增套件
- 選擇 FirebaseMessaging 並新增至目標 ImageNotification:新增套件 
Objective-C
請在 Podfile 中加入新擴充功能,確保該擴充功能可以存取 Firebase/Messaging Pod:
- 從 Navigator 開啟 Podfile:依序點選「Pods」>「Podfile」 
- 前往檔案底部並新增: - target 'ImageNotification' do use_frameworks! pod 'Firebase/Auth' # Add this line if you are using FirebaseAuth phone authentication pod 'Firebase/Messaging' end
- 使用 - pod install從- ios或- macos目錄安裝或更新 Pod。
步驟 3 - 使用擴充功能輔助程式
此時,所有項目都應仍正常運作。最後一步是叫用擴充功能輔助程式。
Swift
- 在導覽器中選取 ImageNotification 擴充功能 
- 開啟 - NotificationService.swift檔案。
- 將 - NotificationService.swift的內容替換為:- import UserNotifications import FirebaseMessaging class NotificationService: UNNotificationServiceExtension { var contentHandler: ((UNNotificationContent) -> Void)? var bestAttemptContent: UNMutableNotificationContent? override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { self.contentHandler = contentHandler bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent) Messaging.serviceExtension().populateNotificationContent(bestAttemptContent!, withContentHandler: contentHandler) } override func serviceExtensionTimeWillExpire() { if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent { contentHandler(bestAttemptContent) } } }
Objective-C
- 在導覽器中選取 ImageNotification 擴充功能 
- 開啟 - NotificationService.m檔案。
- 在檔案頂端,於 - NotificationService.h後方匯入- FirebaseMessaging.h。- 將 - NotificationService.m的內容替換為:- #import "NotificationService.h" #import "FirebaseMessaging.h" #import <FirebaseAuth/FirebaseAuth-Swift.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 () <NSURLSessionDelegate> @property(nonatomic) void (^contentHandler)(UNNotificationContent *contentToDeliver); @property(nonatomic) 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:將圖片新增至酬載
您現在可以在通知酬載中新增圖片。詳情請參閱如何建立傳送要求。