在 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 不需要個別隔離物件),讓您即使在應用程式未執行時也能處理訊息。

使用背景訊息處理常式時,請留意下列事項:

  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());
}

由於處理常式會在應用程式內容外獨立執行,因此無法更新應用程式狀態或執行任何影響 UI 的邏輯。不過,您可以執行諸如 HTTP 要求這類邏輯、執行 IO 作業 (例如更新本機儲存空間)、與其他外掛程式通訊等。

建議您盡快完成邏輯。執行長時間且密集的工作會影響裝置效能,並可能導致 OS 終止程序。如果任務執行時間超過 30 秒,裝置可能會自動終止程序。

網路

在網頁上,編寫在背景執行的 JavaScript 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 變數。

接著,您必須註冊 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 應用程式。系統會註冊 worker,並透過這個檔案處理所有背景訊息。

處理互動

通知是可見的提示,因此使用者常常按下通知與他們互動。Android 和 iOS 裝置的預設行為都是開啟應用程式。如果應用程式已終止,系統會啟動該應用程式;如果應用程式處於背景執行,系統會將其移至前景。

視通知內容而定,您可能會想在應用程式開啟時處理使用者的互動行為。舉例來說,如果使用者按下透過通知傳送的新聊天訊息,您可能會在應用程式開啟時開啟特定對話。

firebase-messaging 套件提供兩種處理這項互動的方式:

  • getInitialMessage():如果應用程式是在已終止的狀態下開啟,系統會傳回包含 RemoteMessageFuture。使用完畢後,RemoteMessage 就會移除。
  • onMessageOpenedApp:在應用程式從背景狀態開啟時,會發布 RemoteMessageStream

建議您處理這兩種情況,確保使用者享有流暢的使用者體驗。以下程式碼範例說明如何達成這項目標:

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. 在伺服器酬載中,針對本地化訊息使用 title_loc_keybody_loc_key,並設為顯示訊息的 name 屬性,而不是使用 titlemessagebody 金鑰。

    訊息酬載會如下所示:

    {
      "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 SQL 分析資料、將資料匯出至其他雲端服務供應商,或將資料用於自訂 ML 模型。匯出至 BigQuery 的資料包含訊息的所有可用資料,不論訊息類型為何,或訊息是透過 API 或通知編寫工具傳送,皆是如此。

如要啟用匯出功能,請先按照這裡所述步驟操作,然後按照下列操作說明進行:

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 等打包工具,才能讓服務工作站運作。請參閱示例應用程式,瞭解如何達成這項目標。

遷移至 v9 SDK 後,您可以使用下列程式碼:

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

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

別忘了執行 yarn build,將新版服務工作者匯出至 web 資料夾。

在 iOS 通知中顯示圖片

在 Apple 裝置上,如要讓收到的 FCM 通知顯示 FCM 酬載的圖片,您必須額外新增通知服務擴充功能,並設定應用程式使用該擴充功能。

如果您使用 Firebase 電話驗證功能,必須將 Firebase 驗證 Pod 新增至您的 Podfile。

步驟 1 - 新增通知服務擴充功能

  1. 在 Xcode 中,依序按一下「File」>「New」>「Target...」
  2. 互動視窗會顯示可能的目標清單,請向下捲動或使用篩選器選取「Notification Service Extension」。點選「下一步」
  3. 新增產品名稱 (使用「ImageNotification」即可按照本教學課程操作),將語言設為 Objective-C,然後按一下「Finish」
  4. 按一下「啟用」啟用配置。

步驟 2 - 將目標新增至 Podfile

請在 Podfile 中新增 Firebase/Messaging Pod,確保新擴充功能可存取該 Pod:

  1. 在導覽器中開啟 Podfile:依序點選「Pods」>「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. 在檔案頂端,將 FirebaseMessaging.h 匯入 NotificationService.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 的映像檔大小上限。