+26

[Flutter] - Hiểu rõ hơn về Firebase Cloud Message (FCM)

Giới thiệu

Ở bài viết này mình sẽ tập trung nhiều vào việc hiểu rõ hơn về Firebase Cloud Message (FCM) thay vì các bước cấu hình để tích hợp FCM vào Flutter project.

Firebase Cloud Messaging (FCM) là gì?

FCM là một dịch vụ gửi thông báo, tin nhắn đa nền tảng được cung cấp bởi Google. Bạn có thể gửi message đến các thiết bị đã đăng ký với FCM, nội dung gửi đi có thể lên đến 4KB.

Một số use-case phổ biến:

  • Hiển thị một thông báo (notification)
  • Đồng bộ dữ liệu trên thiết bị (ex: chạy ngầm để đồng bộ data lưu ở shared_preferences)
  • Cập nhật giao diện (UI) của ứng dụng

Các trạng thái của ứng dụng khi nhận thông báo

Tuỳ thuộc vào trạng thái của ứng dụng trên device, các message gửi đến sẽ được handle khác nhau. Trước tiên ta cần tìm hiểu về các trạng thái của ứng dụng:

State Description
Foreground Khi ứng dụng đang mở và đang được sử dụng
Background Khi ứng dụng đang chạy nhưng nằm ở background. Ví dụ như khi user bấm nút "home" hoặc switch sang một ứng dụng khác
Terminated Khi device bị khoá màn hình, ứng dụng chưa được chạy hoặc bị (đóng hoàn toàn) terminate khỏi background

Có một vài điều kiện tiên quyết cần có để ứng dụng của bạn có thể nhận được message từ FCM:

  • Ứng dụng phải đã được mở ít nhất một lần (để đăng ký với FCM)
  • Đối iOS, bạn phải setup project để tích hợp APN (Apple Push Notification) và FCM
  • Mặc định, nếu ứng dụng ở trạng thái foreground, ứng dụng sẽ không hiển thị thông báo (heads up).

Yêu cầu permission

Đối với Android, mặc định bạn không cần yêu cầu permission. Ngược lại, với iOS bạn phải yêu cầu permission từ user trước khi có thể gửi message từ FCM. Chúng ta có thể làm như sau:

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

Có 4 giá trị cho authorizationStatus:

  • authorized: User đã cấp quyền
  • denied: User từ chối cấp quyền
  • notDetermined: User chưa quyết định cấp quyền hay không
  • provisional: User cấp quyền tạm thời

Đối với Android, mặc định giá trị của authorizationStatus sẽ là authorized nếu user chưa disable notification của ứng dụng.

Handle message từ FCM

Sau khi đã có permission và hiểu về các trạng thái của ứng dụng, bây giờ chúng ta có thể bắt đầu handle các message được gửi đền từ FCM.

Message type

Có 3 kiểu message:

  • Notification only message: payload kiểu này chỉ chứa một thuộc tính notification, cái sẽ dùng để hiển thị một thông báo trên máy người dùng.
  • Data only message (silent message): payload kiểu này sẽ có thuộc tính data, là các cặp key/value. Các message này được xem là low priority.
  • Notification & Data message: Payload sẽ có cả 2 thuộc tính là notificationdata.

Dựa trên trạng thái của ứng dụng, các message sẽ cần được handle khác nhau:

Foreground Background Terminated
Notification onMessage onBackgroundMessage onBackgroundMessage
Data onMessage onBackgroundMessage (*lưu ý) onBackgroundMessage(*lưu ý)
Notification & Data onMessage onBackgroundMessage onBackgroundMessage

Lưu ý: Message dạng data only được xem như low priority nên sẽ bị phớt lờ khi ứng dụng ở trạng thái background hoặc terminated. Tuy nhiên, bạn có thể tăng mức độ ưu tiên của message trước khi gửi đi. Ví dụ từ phía server Node.js:

admin
  .messaging()
  .sendToDevice(
    [deviceToken],
    {
      data: {
        foo:'bar',
      },
      notification: {
        title: 'A great title',
        body: 'Great content',
      },
    },
    {
      // Required for background/terminated app state messages on iOS
      contentAvailable: true,
      // Required for background/terminated app state messages on Android
      priority: 'high',
    }
  )

Đối với Foreground messages

Để lắng nghe các message trong khi ứng dụng ở trạng thái foreground, có thể dùng hàm onMesssage:

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

Lưu ý: Như mình có nói trước đó, khi ứng dụng ở trạng thái foreground, sẽ không hiển thị thông báo khi nhận được message từ FCM. Mình sẽ hướng dẫn cách khác phục bến dưới.

Đối với background messages

Bạn có thể dùng hàm onBackgroundMessage để handle các message được gửi đến trong khi ứng dụng ở trạng thái background hoặc terminated.

@pragma('vm:entry-point')
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
  await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
  await setupFlutterNotifications();
  showFlutterNotification(message);
  // 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.
  print('Handling a background message ${message.messageId}');
}

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);

  // Set the background messaging handler early on, as a named top-level function
  FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);

  if (!kIsWeb) {
    await setupFlutterNotifications();
  }

  runApp(MessagingExampleApp());
}

Có 3 điều cần lưu ý đối với hàm xử lý background message (hàm _firebaseMessagingBackgroundHandler ở ví dụ trên):

  • Không thể là anonymous function
  • Phải là top-level function (không phải method của một class)
  • Phải có annotation @pragma('vm:entry-point') phía trên function

Khi có một message có dạng notification được gửi đến user, Firebase SDK sẽ nhận ra và hiển thị một thông báo cho user (nếu user đã được cấp permission). Khi đó, hàm _firebaseMessagingBackgroundHandler ở trên sẽ được thực thi.

Low priority messages

Như mình đã đề cập ở trên, message dạng data only được xem là low priority. Các thiết bị có thể phớt lờ chúng nếu ứng dụng của bạn ở background hoặc terminated hoặc trong trạng thái tiết kiệm năng lượng, hiệu năng.

Hiển thị thông báo ở Foreground (Heads up)

Mặc dù ứng dụng vẫn nhưng được message (data/notification) từ FCM nhưng sẽ không hiển thị bất kì thông báo nào trên device. Đối với iOS: Để hiện thị heads up, chúng ta chỉ cần gọi hàm dưới đây khi khởi tạo FCM:

await FirebaseMessaging.instance.setForegroundNotificationPresentationOptions(
  alert: true, // Required to display a heads up notification
  badge: true,
  sound: true,
);

Đối với Android: Trước tiên bạn cần hiểu qua cơ chế hiển thị thông báo của FCM trên Android. FCM sẽ sử dụng một channel (Notification Channel) mặc định của nó để xác định cách thông báo được hiển thị:

  • Nếu ứng dụng ở trạng thái background hoặc terminated, device xuất hiện thông báo khi nhận được từ FCM.
  • Nếu ứng dụng ở trạng thái foreground, mặc định bạn đang sử dụng ứng dụng nên thông báo sẽ không xuất hiện. (Lý do Firebase giải thích là trong trường hợp này bạn nên dùng In-App Message để gửi đến user thay vì Notification Message mà ta hay dùng)

Nhưng tất nhiên mục tiêu của chúng ta ở đây là để hiển thị được thông báo dạng heads up cho dù đang ở trạng thái Foreground.

Chúng ta cần tạo một channel mới với thuộc tính importancemax. Sau đó truyền các message từ FCM đến channel. Và dùng các thư viện hiển thị thông báo khác hổ trợ, ví dụ như là flutter_local_notifications. Như tên của nó, package này sẽ giúp bạn hiển thị thông báo trên device người dùng.

  1. Đầu tiên thêm package flutter_local_notificationsvào project của bạn
  2. Khởi tạo một đối tượng channel với thuộc tínhimportance là cao nhất (max) và id là "high_importance_channel":
const AndroidNotificationChannel channel = AndroidNotificationChannel(
  'high_importance_channel', // id
  'High Importance Notifications', // title
  'This channel is used for important notifications.', // description
  importance: Importance.max,
);
  1. Tạo một Notification Channel trên device từ đối tượng channel đã tạo ở trên:
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
    FlutterLocalNotificationsPlugin();

await flutterLocalNotificationsPlugin
  .resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>()
  ?.createNotificationChannel(channel);
  1. Cấu hình cho FCM sử dụng channel của mình thay vì mặc định. Tại file android/app/src/main/AndroidManifest.xml, sử dụng channel có id là "high_importance_channel" mà ta đã tạo ở trên:
<meta-data
  android:name="com.google.firebase.messaging.default_notification_channel_id"
  android:value="high_importance_channel" />
  1. Handle hiển thị thông báo từ FCM:

Lúc này message từ FCM đã được gửi đến device qua channel mà ta vừa tạo và dưới level importance là max. Tuy nhiên, lúc này bạn sẽ vẫn không thấy thông báo được hiển thị. Bởi vì Firebase Android SDK sẽ không cho phép hiển thị bất kì thông báo nào từ FCM cho dù nó dùng channel nào. Đó là lúc phát huy công dụng.

Sử dụng hàm OnMessage để lắng nghe các message từ FCM . Từ đó lấy được data của message, sau đó dùng packageflutter_local_notifications để hiển thị thông báo.

FirebaseMessaging.onMessage.listen((RemoteMessage message) {
  RemoteNotification notification = message.notification;
  AndroidNotification android = message.notification?.android;

  // If `onMessage` is triggered with a notification, construct our own
  // local notification to show to users using the created channel.
  if (notification != null && android != null) {
    flutterLocalNotificationsPlugin.show(
        notification.hashCode,
        notification.title,
        notification.body,
        NotificationDetails(
          android: AndroidNotificationDetails(
            channel.id,
            channel.name,
            channel.description,
            icon: android?.smallIcon,
            // other properties...
          ),
        ));
  }
});

Cơ chế gửi thông báo theo topic

Ngoài việc gửi một message đến một thiết bị cụ thể theo device token, bạn có thể gửi theo các topic và thế là bất kì thiệt bị nào đã subcribe các topic đó sẽ nhận được. Cơ chế cho phép một thiết bị subcribe hoặc unsubcribe các channel PubSub được đặt tên, tất cả được quản lý bởi FCM.

Vì chỉ quan tâm đến các topic, cơ chế này cho giúp bạn đơn giản hoá việc gửi message/notification từ server bằng cách không cần lưu trữ các device token. Tuy nhiên có một số điều cần lưu ý như sau:

  • Các message gửi đến topic không nên chứa các thông tin nhạy cảm hoặc riêng tư. Do nên mỗi topic cho mỗi user
  • Topic messaging hỗ trợ không giới hạn số lượng subcription cho mỗi topic
  • Một app instance có thể subcribe đến 2000 topic
  • Một topic được tạo ra khi nó được subscribe lần đầu
  • Nếu gửi quá nhiều request subscription liên tục, FCM có thể trả về lỗi 429 RESOURCE_EXHAUSTED
  • Server có thể gửi một message đến tối đa 5 topic một lúc.

Subscribe topic

Để subscribe một topic, ví dụ topic "weather":

// subscribe to topic on each app start-up
await FirebaseMessaging.instance.subscribeToTopic('weather');

Unsubscribe topic

await FirebaseMessaging.instance.unsubscribeFromTopic('weather');

Gửi message đến một topic

Server có thể gửi message/notification đến một topic để tất cả các thiết bị đã subcribe topic đấy có thể nhận được message/notification. Ex: Ở server Node.js, sử dụng firebase-admin để gửi một message đến topic "weather".

// Node.js e.g via a Firebase Cloud Function
const admin = require("firebase-admin");

const message = {
  data: {
    type: "warning",
    content: "A new weather warning has been created!",
  },
  topic: "weather",
};

admin
  .messaging()
  .send(message)
  .then((response) => {
    console.log("Successfully sent message:", response);
  })
  .catch((error) => {
    console.log("Error sending message:", error);
  });

Ngoài ra chúng ra có thể sử dụng boolean expression xác định điều kiện cho các topic sẽ nhận được message/notification. Ex: Ví dụ bên dưới sẽ chỉ gửi message đến các device đã subcribe topic "weather" và "news" hoặc "weather" và "traffic".

const admin = require("firebase-admin");

const message = {
  data: {
    content: "New updates are available!",
  },
  condition: "'weather' in topics && ('news' in topics || 'traffic' in topics)",
};

admin
  .messaging()
  .send(message)
  .then((response) => {
    console.log("Successfully sent message:", response);
  })
  .catch((error) => {
    console.log("Error sending message:", error);
  });

Nguồn tham khảo:

https://firebase.flutter.dev/docs/messaging/notifications#displaying-notifications

Example project:

https://github.com/firebase/flutterfire/tree/master/packages/firebase_messaging/firebase_messaging/example


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.