Introduce
In this article, I will focus more on better understanding Firebase Cloud Message (FCM) instead of configuration steps to integrate FCM into Flutter project.
What is Firebase Cloud Messaging (FCM)?
FCM is a cross-platform messaging and notification service provided by Google. You can send messages to devices registered with FCM, the sent content can be up to 4KB.
Some common use-cases:
- Show a notification
- Synchronize data on the device (ex: run in the background to synchronize data stored in shared_preferences)
- Update the interface (UI) of the application
Application states when receiving notifications
Depending on the state of the application on the device, incoming messages will be handled differently. First we need to understand the state of the application:
State | description |
---|---|
Foreground | When the app is open and in use |
Background | When the application is running but in the background. For example, when the user presses the “home” button or switches to another application |
Terminated | When the device is locked from the screen, the application is not launched or is (fully closed) terminated from the background |
There are a few prerequisites that are required for your application to receive messages from FCM:
- Application must have been opened at least once (to register with FCM)
- For iOS, you must setup the project to integrate APN (Apple Push Notification) and FCM
- By default, if the application is in the foreground state, the application will not show notifications (heads up).
Request permission
For Android, by default you don’t need to ask for permission. In contrast, with iOS you must request permission from the user before you can send messages from FCM. We can do the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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}'); |
There are 4 values for authorizationStatus
:
authorized
: User has granted permissiondenied
: User refuses to grant permissionnotDetermined
: User has not decided to grant permission or notprovisional
: User grants temporary permission
For Android, the default value of authorizationStatus
will be authorized
if the user has not disabled the application’s notification.
Handle messages from FCM
After we have permission and understand the state of the application, we can now start handling messages sent from FCM.
Message type
There are 3 types of messages:
- Notification only message: This type of payload contains only a
notification
attribute, which will be used to display a message on the user’s machine. - Data only message (silent message): payload of this type will have a
data
attribute, which is key/value pairs. These messages are consideredlow priority
. - Notification & Data message: Payload will have both
notification
anddata
properties.
Based on the state of the application, messages will need to be handled differently:
Foreground | Background | Terminated | |
---|---|---|---|
Notification | onMessage |
onBackgroundMessage |
onBackgroundMessage |
Data | onMessage |
onBackgroundMessage (*note) |
onBackgroundMessage (*note) |
Notifications & Data | onMessage |
onBackgroundMessage |
onBackgroundMessage |
Note : Messages of data only
type are considered as low priority
, so they will be ignored when the application is in the background
or terminated
. However, you can increase the priority of the message before sending it. Example from Node.js server side:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
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', } ) |
For Foreground messages
To listen for messages while the application is in the foreground state, the onMesssage
function can be used:
1 2 3 4 5 6 7 8 9 |
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}'); } }); |
Note: As I said earlier, when the application is in the foreground state, it will not display a notification when receiving a message from FCM. I will show you another way to restore the dock below.
For background messages
You can use the onBackgroundMessage
function to handle incoming messages while the application is in the background
or terminated
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
@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()); } |
There are 3 things to note about the background message handler function (the _firebaseMessagingBackgroundHandler
function in the example above):
- Cannot be anonymous function
- Must be a top-level function (not a method of a class)
- There must be
@pragma('vm:entry-point')
annotation above the function
When a notification-type message is sent to the user, the Firebase SDK will recognize and display a notification to the user (if the user has been granted permission). Then, the above _firebaseMessagingBackgroundHandler
function will be executed.
Low priority messages
As I mentioned above, data only
messages are considered low priority
. Devices can ignore them if your app is in the background
or terminated
or in a low power, performance state.
Display message in Foreground (Heads up)
Although the application still receives messages (data/notification) from FCM, it will not display any notifications on the device. For iOS : To show heads up, we just need to call below function when initializing FCM:
1 2 3 4 5 6 |
await FirebaseMessaging.instance.setForegroundNotificationPresentationOptions( alert: true, // Required to display a heads up notification badge: true, sound: true, ); |
For Android : First you need to understand through the mechanism of displaying FCM notifications on Android. FCM will use its default channel (Notification Channel) to determine how notifications are displayed:
- If the application is in the background or terminated , the device shows a notification when it receives it from the FCM.
- If the app is in the foreground state, you’re using the app by default, so the notification won’t appear. (The reason Firebase explains is that in this case you should use In-App Message to send to the user instead of the Notification Message that we often use)
But of course our goal here is to show heads up notifications even in the Foreground state.
We need to create a new channel with the importance
attribute set to max
. Then transmit messages from the FCM to the channel. And use other notification display libraries that support, for example flutter_local_notifications
. As the name implies, this package will help you to display notifications on the user’s device.
- First add package
flutter_local_notifications
to your project - Initialize a channel object with the highest
importance
property (max) and theid
“high_importance_channel”:
1 2 3 4 5 6 7 |
const AndroidNotificationChannel channel = AndroidNotificationChannel( 'high_importance_channel', // id 'High Importance Notifications', // title 'This channel is used for important notifications.', // description importance: Importance.max, ); |
- Create a Notification Channel on the device from the channel object created above:
1 2 3 4 5 6 7 |
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); await flutterLocalNotificationsPlugin .resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>() ?.createNotificationChannel(channel); |
- Configure FCM to use your channel instead of the default. In the file
android/app/src/main/AndroidManifest.xml
, use the channel with theid
“high_importance_channel” that we created above:
1 2 3 4 |
<meta-data android:name="com.google.firebase.messaging.default_notification_channel_id" android:value="high_importance_channel" /> |
- Handle displays the message from FCM:
Now the message from FCM has been sent to the device through the channel we just created and is below the level of importance max. However, at this point you will still not see the message displayed. Because Firebase Android SDK will not allow to display any message from FCM no matter what channel it uses. That’s when it comes to use.
Use the OnMessage
function to listen for messages from the FCM . From there get the data of the message, then use the flutter_local_notifications
package to display the notification.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
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... ), )); } }); |
Mechanism of sending notifications by topic
In addition to sending a message to a specific device by device token, you can send by topics and so any device that has subscribed to those topics will receive them. The mechanism that allows a device to subscribe or unsubscribe to named PubSub channels, all managed by the FCM.
Since it is only interested in topics, this mechanism allows you to simplify sending messages/notifications from the server by not needing to store device tokens. However, there are a few things to keep in mind:
- Messages sent to the topic should not contain sensitive or private information. So each topic for each user
- Topic messaging supports unlimited number of subscriptions per topic
- One app instance can subscribe to 2000 topics
- A topic is created when it is first subscribed
- If sending too many subscription requests continuously, FCM may return error 429 RESOURCE_EXHAUSTED
- The server can send a message to up to 5 topics at a time.
Subscribe topic
To subscribe to a topic, for example the topic “weather”:
1 2 3 |
// subscribe to topic on each app start-up await FirebaseMessaging.instance.subscribeToTopic('weather'); |
Unsubscribe topic
1 2 |
await FirebaseMessaging.instance.unsubscribeFromTopic('weather'); |
Send a message to a topic
The server can send a message/notification to a topic so that all devices that have subscribed to that topic can receive the message/notification. Ex: On the Node.js server, use firebase-admin
to send a message to the “weather” topic.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
// 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); }); |
In addition, we can use a boolean expression to define the conditions for topics that will receive messages/notifications. Ex: The example below will only send messages to devices that have subscribed to topics “weather” and “news” or “weather” and “traffic”.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
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); }); |
Reference source:
https://firebase.flutter.dev/docs/messaging/notifications#displaying-notifications