[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ềndenied
: User từ chối cấp quyềnnotDetermined
: User chưa quyết định cấp quyền hay khôngprovisional
: 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à
notification
vàdata
.
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 importance
là max
. 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.
- Đầu tiên thêm package
flutter_local_notifications
vào project của bạn - Khởi tạo một đối tượng channel với thuộc tính
importance
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,
);
- 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);
- 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" />
- 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:
All rights reserved