Nhận tin nhắn trong ứng dụng Flutter

Tuỳ thuộc vào trạng thái của thiết bị, các tin nhắn đến sẽ được xử lý khác nhau. Để hiểu những trường hợp này và cách tích hợp FCM vào ứng dụng của riêng bạn, trước tiên, bạn phải thiết lập các trạng thái khác nhau mà một thiết bị có thể ở:

Tiểu bang Mô tả
Nền trước Khi ứng dụng đang mở, ở chế độ xem và đang được sử dụng.
Thông tin cơ bản Khi ứng dụng đang mở nhưng chạy trong nền (bị thu nhỏ). Điều này thường xảy ra khi người dùng nhấn nút "màn hình chính" trên thiết bị, đã chuyển sang một ứng dụng khác bằng trình chuyển đổi ứng dụng hoặc mở ứng dụng trong một thẻ khác (web).
Đã chấm dứt Khi thiết bị bị khoá hoặc ứng dụng hiện không chạy.

Bạn phải đáp ứng một số điều kiện tiên quyết trước khi ứng dụng có thể nhận được các tải trọng tin nhắn qua FCM:

  • Ứng dụng phải đã mở ít nhất một lần (để cho phép đăng ký với FCM).
  • Trên iOS, nếu người dùng vuốt ứng dụng khỏi trình chuyển đổi ứng dụng, thì bạn phải mở lại ứng dụng theo cách thủ công để thông báo trong nền tiếp tục hoạt động.
  • Trên Android, nếu người dùng buộc thoát khỏi ứng dụng trong phần cài đặt thiết bị, thì bạn phải mở lại ứng dụng theo cách thủ công để thông báo bắt đầu hoạt động.
  • Trên web, bạn phải yêu cầu một mã thông báo (sử dụng getToken()) kèm theo chứng chỉ về thông báo đẩy trên web.

Yêu cầu cấp quyền để nhận tin nhắn

Trên iOS, macOS, web và Android 13 (trở lên), trước khi có thể nhận các tải trọng FCM trên thiết bị, trước tiên, bạn phải xin phép người dùng.

Gói firebase_messaging cung cấp một API đơn giản để yêu cầu quyền thông qua phương thức requestPermission. API này chấp nhận một số đối số được đặt tên xác định loại quyền mà bạn muốn yêu cầu, chẳng hạn như liệu tin nhắn chứa tải trọng thông báo có thể kích hoạt âm thanh hoặc đọc to tin nhắn qua Siri hay không. Theo mặc định, phương thức này yêu cầu các quyền mặc định hợp lý. API tham chiếu cung cấp tài liệu đầy đủ về chức năng của từng quyền.

Để bắt đầu, hãy gọi phương thức từ ứng dụng của bạn (trên iOS, một phương thức gốc sẽ hiển thị, trên web, luồng API gốc của trình duyệt sẽ được kích hoạt):

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

Bạn có thể dùng thuộc tính authorizationStatus của đối tượng NotificationSettings được trả về từ yêu cầu để xác định quyết định tổng thể của người dùng:

  • authorized: Người dùng đã cấp quyền.
  • denied: Người dùng đã từ chối cấp quyền.
  • notDetermined: Người dùng chưa chọn có cấp quyền hay không.
  • provisional: Người dùng đã cấp quyền tạm thời

Các thuộc tính khác trên NotificationSettings trả về cho biết liệu một quyền cụ thể được bật, tắt hay không được hỗ trợ trên thiết bị hiện tại.

Sau khi cấp quyền và hiểu được các loại trạng thái thiết bị, ứng dụng của bạn giờ có thể bắt đầu xử lý các tải trọng FCM đến.

Xử lý tin nhắn

Dựa trên trạng thái hiện tại của ứng dụng, các tải trọng đến thuộc nhiều loại thông báo sẽ yêu cầu những cách triển khai khác nhau để xử lý:

Thông báo trên nền trước

Để xử lý thông báo khi ứng dụng chạy trên nền trước, hãy nghe luồng 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}');
  }
});

Luồng (stream) chứa RemoteMessage, trình bày chi tiết nhiều thông tin về tải trọng, chẳng hạn như nguồn gốc của tải trọng, mã nhận dạng duy nhất, thời gian gửi, liệu nội dung có chứa thông báo hay không, v.v. Vì thông báo được truy xuất trong khi ứng dụng đang chạy ở nền trước, nên bạn có thể truy cập trực tiếp vào trạng thái và ngữ cảnh của ứng dụng Flutter.

Thông báo và thông báo trên nền trước

Theo mặc định, các thông báo xuất hiện khi ứng dụng đang chạy trên nền trước sẽ không hiện thông báo hiển thị trên cả Android và iOS. Tuy nhiên, bạn có thể ghi đè hành vi này:

  • Trên Android, bạn phải tạo kênh thông báo "Mức độ ưu tiên cao".
  • Trên iOS, bạn có thể cập nhật các tuỳ chọn trình bày cho ứng dụng.

Thông báo trong nền

Quy trình xử lý thông báo trong nền sẽ khác nhau trên các nền tảng gốc (Android và Apple) và nền tảng web.

Các nền tảng của Apple và Android

Xử lý thông báo trong nền bằng cách đăng ký trình xử lý onBackgroundMessage. Khi nhận được thư, một dữ liệu phân tách sẽ được tạo (chỉ dành cho Android, iOS/macOS không yêu cầu một vùng cách ly riêng) cho phép bạn xử lý thư ngay cả khi ứng dụng của bạn hiện không chạy.

Có một vài điều cần lưu ý về trình xử lý thông báo nền:

  1. Không được là chức năng ẩn danh.
  2. Đây phải là một hàm cấp cao nhất (ví dụ: không phải là phương thức lớp yêu cầu khởi chạy).
  3. Khi sử dụng Flutter phiên bản 3.3.0 trở lên, trình xử lý thông báo phải được chú thích bằng @pragma('vm:entry-point') ngay phía trên phần khai báo hàm (nếu không thì trình xử lý thông báo có thể bị xoá trong quá trình lắc cây đối với chế độ phát hành).
@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());
}

Vì trình xử lý chạy trong vùng tách biệt riêng bên ngoài ngữ cảnh ứng dụng của bạn, nên không thể cập nhật trạng thái ứng dụng hoặc thực thi bất kỳ logic tác động đến giao diện người dùng nào. Tuy nhiên, bạn có thể thực hiện logic như yêu cầu HTTP, thao tác IO (ví dụ: cập nhật bộ nhớ cục bộ), giao tiếp với các trình bổ trợ khác, v.v.

Bạn cũng nên hoàn thành logic càng sớm càng tốt. Việc chạy các tác vụ mất nhiều thời gian, tốn nhiều công sức sẽ ảnh hưởng đến hiệu suất của thiết bị và có thể khiến hệ điều hành chấm dứt quy trình. Nếu các tác vụ chạy lâu hơn 30 giây, thì thiết bị có thể tự động dừng quy trình này.

Web

Trên web, hãy viết Service Worker của JavaScript chạy ở chế độ nền. Sử dụng trình chạy dịch vụ để xử lý các thông báo trong nền.

Để bắt đầu, hãy tạo một tệp mới trong thư mục web và gọi tệp đó là firebase-messaging-sw.js:

importScripts("https://www.gstatic.com/firebasejs/8.10.0/firebase-app.js");
importScripts("https://www.gstatic.com/firebasejs/8.10.0/firebase-messaging.js");

firebase.initializeApp({
  apiKey: "...",
  authDomain: "...",
  databaseURL: "...",
  projectId: "...",
  storageBucket: "...",
  messagingSenderId: "...",
  appId: "...",
});

const messaging = firebase.messaging();

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

Tệp này phải nhập cả SDK ứng dụng và SDK nhắn tin, khởi chạy Firebase và hiển thị biến messaging.

Tiếp theo, bạn phải đăng ký worker này. Trong tệp mục nhập, sau khi tệp main.dart.js tải xong, hãy đăng ký worker:

<html>
<body>
  ...
  <script src="main.dart.js" type="application/javascript"></script>
  <script>
       if ('serviceWorker' in navigator) {
          // Service workers are supported. Use them.
          window.addEventListener('load', function () {
            // ADD THIS LINE
            navigator.serviceWorker.register('/firebase-messaging-sw.js');

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

            //  ...
          });
      }
  </script>

Tiếp theo, hãy khởi động lại ứng dụng Flutter. Worker này sẽ được đăng ký và mọi thông báo trong nền sẽ được xử lý thông qua tệp này.

Xử lý tương tác

Vì thông báo là tín hiệu hiển thị nên người dùng thường tương tác với thông báo (bằng cách nhấn). Hành vi mặc định trên cả Android và iOS là mở ứng dụng. Nếu bị chấm dứt thì ứng dụng sẽ được khởi động; nếu chạy trong nền thì ứng dụng sẽ được đưa lên nền trước.

Tuỳ thuộc vào nội dung thông báo, bạn có thể muốn xử lý tương tác của người dùng khi ứng dụng mở. Ví dụ: nếu tin nhắn trò chuyện mới được gửi qua một thông báo và người dùng nhấn vào thông báo đó, thì bạn có thể muốn mở cuộc trò chuyện cụ thể khi ứng dụng mở ra.

Gói firebase-messaging cung cấp 2 cách để xử lý hoạt động tương tác này:

  • getInitialMessage(): Nếu ứng dụng được mở từ trạng thái kết thúc, thì Future chứa RemoteMessage sẽ được trả về. Sau khi sử dụng, RemoteMessage sẽ bị xoá.
  • onMessageOpenedApp: Một Stream đăng RemoteMessage khi mở ứng dụng ở trạng thái nền.

Bạn nên xử lý cả hai trường hợp này để đảm bảo trải nghiệm người dùng mượt mà. Ví dụ về mã dưới đây trình bày cách đạt được điều này:

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

Cách bạn xử lý hoạt động tương tác phụ thuộc vào cách thiết lập ứng dụng của bạn. Ví dụ trên cho thấy hình minh hoạ cơ bản bằng cách sử dụng StatefulWidget.

Bản địa hoá thông báo

Bạn có thể gửi các chuỗi đã bản địa hoá theo hai cách khác nhau:

  • Lưu trữ ngôn ngữ ưu tiên của từng người dùng trong máy chủ của bạn và gửi thông báo tuỳ chỉnh cho từng ngôn ngữ
  • Nhúng các chuỗi đã bản địa hoá vào ứng dụng của bạn và tận dụng chế độ cài đặt ngôn ngữ bản địa của hệ điều hành

Dưới đây là cách sử dụng phương thức thứ hai:

Android

  1. Chỉ định thông báo bằng ngôn ngữ mặc định của bạn trong resources/values/strings.xml:

    <string name="notification_title">Hello world</string>
    <string name="notification_message">This is a message</string>
    
  2. Chỉ định các thông báo đã dịch trong thư mục values-language. Ví dụ: chỉ định các thông điệp bằng tiếng Pháp trong resources/values-fr/strings.xml:

    <string name="notification_title">Bonjour le monde</string>
    <string name="notification_message">C'est un message</string>
    
  3. Trong tải trọng máy chủ, thay vì dùng khoá title, messagebody, hãy dùng title_loc_keybody_loc_key cho thông báo đã bản địa hoá rồi đặt các khoá này thành thuộc tính name của thông báo mà bạn muốn hiển thị.

    Tải trọng tin nhắn sẽ có dạng như sau:

    {
      "data": {
        "title_loc_key": "notification_title",
        "body_loc_key": "notification_message"
      }
    }
    

iOS

  1. Chỉ định thông báo bằng ngôn ngữ mặc định của bạn trong Base.lproj/Localizable.strings:

    "NOTIFICATION_TITLE" = "Hello World";
    "NOTIFICATION_MESSAGE" = "This is a message";
    
  2. Chỉ định các thông báo đã dịch trong thư mục language.lproj. Ví dụ: chỉ định các thông điệp bằng tiếng Pháp trong fr.lproj/Localizable.strings:

    "NOTIFICATION_TITLE" = "Bonjour le monde";
    "NOTIFICATION_MESSAGE" = "C'est un message";
    

    Tải trọng tin nhắn sẽ có dạng như sau:

    {
      "data": {
        "title_loc_key": "NOTIFICATION_TITLE",
        "body_loc_key": "NOTIFICATION_MESSAGE"
      }
    }
    

Bật tính năng xuất dữ liệu gửi thư

Bạn có thể xuất dữ liệu tin nhắn vào BigQuery để phân tích thêm. BigQuery cho phép bạn phân tích dữ liệu bằng BigQuery SQL, xuất dữ liệu đó sang một nhà cung cấp dịch vụ đám mây khác hoặc sử dụng dữ liệu cho các mô hình học máy tuỳ chỉnh. Dữ liệu xuất sang BigQuery bao gồm mọi dữ liệu có sẵn cho tin nhắn, bất kể loại thông báo hoặc việc thông báo được gửi qua API hay trình soạn Thông báo.

Để bật tính năng xuất dữ liệu, trước tiên hãy làm theo các bước mô tả ở đây, sau đó làm theo các hướng dẫn sau:

Android

Bạn có thể dùng đoạn mã sau:

await FirebaseMessaging.instance.setDeliveryMetricsExportToBigQuery(true);

iOS

Đối với iOS, bạn cần thay đổi AppDelegate.m với nội dung sau.

#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

Đối với Web, bạn cần thay đổi trình chạy dịch vụ của mình để sử dụng phiên bản SDK v9. Phiên bản v9 cần được đóng gói, vì vậy, bạn cần sử dụng một trình đóng gói như esbuild chẳng hạn để giúp trình chạy dịch vụ hoạt động. Hãy xem ứng dụng ví dụ để biết cách thực hiện việc này.

Sau khi di chuyển sang SDK phiên bản 9, bạn có thể sử dụng mã sau:

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

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

Đừng quên chạy yarn build để xuất phiên bản mới của trình chạy dịch vụ sang thư mục web.

Hiển thị hình ảnh trong thông báo trên iOS

Trên các thiết bị của Apple, để Thông báo FCM đến hiển thị hình ảnh từ tải trọng FCM, bạn phải thêm tiện ích dịch vụ thông báo bổ sung và định cấu hình ứng dụng của mình để sử dụng tiện ích đó.

Nếu đang sử dụng phương thức xác thực điện thoại Firebase, bạn phải thêm Nhóm xác thực Firebase vào Podfile của mình.

Bước 1 – Thêm tiện ích dịch vụ thông báo

  1. Trong Xcode, hãy nhấp vào File > New > Target... (Tệp > Mới > Mục tiêu...)
  2. Một cửa sổ phụ sẽ hiển thị danh sách các mục tiêu có thể có; hãy cuộn xuống hoặc sử dụng bộ lọc để chọn Tiện ích dịch vụ thông báo. Nhấp vào Tiếp theo.
  3. Thêm tên sản phẩm (sử dụng "ImageNotification" ("Thông báo hình ảnh") để làm theo hướng dẫn này), đặt ngôn ngữ thành Target-C và nhấp vào Finish (Hoàn tất).
  4. Bật lược đồ bằng cách nhấp vào Kích hoạt.

Bước 2 – Thêm mục tiêu vào Podfile

Hãy đảm bảo rằng tiện ích mới của bạn có quyền truy cập vào nhóm Firebase/Messaging bằng cách thêm tiện ích đó vào Podfile:

  1. Trên Trình điều hướng, hãy mở Podfile: Pods > Podfile

  2. Di chuyển xuống cuối tệp rồi thêm:

    target 'ImageNotification' do
      use_frameworks!
      pod 'Firebase/Auth' # Add this line if you are using FirebaseAuth phone authentication
      pod 'Firebase/Messaging'
    end
    
  3. Cài đặt hoặc cập nhật các nhóm của bạn bằng cách sử dụng pod install từ thư mục ios hoặc macos.

Bước 3 – Sử dụng trình trợ giúp tiện ích

Tại thời điểm này, mọi thứ vẫn sẽ chạy bình thường. Bước cuối cùng là gọi trình trợ giúp tiện ích.

  1. Từ trình điều hướng, hãy chọn tiện ích ImageNotification của bạn

  2. Mở tệp NotificationService.m.

  3. Ở đầu tệp, hãy nhập FirebaseMessaging.h ngay sau NotificationService.h như minh hoạ dưới đây.

    Thay thế nội dung của NotificationService.m bằng:

    #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
    

Bước 4 – Thêm hình ảnh vào tải trọng

Trong tải trọng thông báo, giờ đây bạn có thể thêm hình ảnh. Hãy xem tài liệu dành cho iOS về cách tạo một yêu cầu gửi. Xin lưu ý rằng thiết bị phải thực thi kích thước hình ảnh tối đa là 300KB.