firebase_messaging
| Package | Kind | Provides the role | Needs | Choose with |
|---|---|---|---|---|
smf_firebase_messaging | infrastructure | Push messages | firebase_core, notifications | -m firebase_messaging |
firebase_messaging receives push messages with Firebase Cloud Messaging, through the firebase_messaging package. The app gets the address of the device for its backend, receives the messages that the backend sends there, and hears of the taps on them, including the tap that launched the app.
The code of the app never uses the package. It uses appPush, which the push role generates; see Push messages. The role shows a message that arrives in the foreground as a notification of the app, so it needs a module of notifications, and smf create adds local_notifications with this module.
What it adds to the app
| In the app | What it is |
|---|---|
firebase_messaging: ^16.7.0 in the dependencies | The package of Firebase Cloud Messaging. 16.7 is the version that works with firebase_core 4, which the Firebase modules share. |
lib/core/push/firebase_push_service.dart | FirebasePushService, the push service of the app on FirebaseMessaging.instance, and createFirebasePushService(), which creates it. |
Two <meta-data> entries in android/app/src/main/AndroidManifest.xml | The channel and the small icon of a message that Android shows in the background. |
| The section "Firebase Cloud Messaging" of the README | The key of the Apple Push Notification service, a test message, what the module set up for Android and iOS, and messages in the background. |
The step by hand firebase_messaging.apns_key | The upload of the APNs key to the Firebase project; see the APNs key. |
Whichever module provides it, the push role adds:
| In the app | What it is |
|---|---|
lib/core/push/app_push.dart | appPush, through which the code of the app reads the address of the device, the messages and the taps, and initPush(). |
lib/core/push/push_service.dart | PushService, the interface that this module implements, and PushMessage. |
The entitlement aps-environment with the value development in ios/Runner/Runner.entitlements | It lets the iOS app receive push messages. Only a paid Apple Developer team signs it, so the role also lists the step by hand app_entry.ios_app_id. |
remote-notification among the UIBackgroundModes of ios/Runner/Info.plist | It lets iOS wake the app for a message that arrives in the background. |
In its platform phase, bootstrap() initializes Firebase, opens the preferences and the notifications, and then starts the push messages. This is bootstrap() of an app with this module and the modules that come with it:
Future<void> bootstrap() async {
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
await initPreferences();
await initNotifications();
await initPush();
}
The service is created without waiting, and asks the user nothing. appPush asks it for the address without waiting for the answer, so the first frame never waits for the network. The message whose tap launched the app is known once bootstrap() is over.
In the guide for coding agents of the app, the push role describes appPush under "Push messages". The module adds which file is on firebase_messaging, where the address comes from and why the service waits for it on iOS, why it sets the presentation options of iOS, that the app never calls requestPermission() of the package, what the manifest names for Android, and what a test that runs bootstrap() answers in place of the platform side of the package. The README of the app gets the section "Firebase Cloud Messaging" from the module and the section "Push messages" from the role.
The address of the device
The address in appPush.address is the registration token of Firebase Cloud Messaging on the device, from getToken(). A new token comes on onTokenRefresh, and appPush.address follows it. Send each new address to your backend, which sends the messages there; see Push messages.
The address is null while Firebase gives no token. Android has none without Google Play services or a network, and iOS has none before the device has a token of the Apple Push Notification service (APNs). In debug mode, the app prints why it has no address yet, and then each new address:
The address of this device for push messages: <token>
The wait on iOS
iOS gives the device its APNs token a while after the app started, or never, as for a build that is not signed for push messages. Firebase gives no registration token before that, and when the APNs token comes, firebase_messaging tells the app nothing. So while appPush listens for new addresses, which it does from its start, the service asks iOS for the APNs token, a read without the network. It waits half a second before the first try and twice as long before each next one, up to eight seconds, for at most two minutes in all. Once iOS has the token, the service tells of the registration token. After two minutes it stops, and appPush asks again each time the user comes back to the app while it has no address.
Only a build signed with the entitlement aps-environment gets an APNs token. The iOS simulator on a Mac gets one, one on a macOS virtual machine never does, and on an iPhone the signing needs a team of the paid Apple Developer Program; see Push messages.
Messages in the foreground and in the background
A message that arrives while the app is in the foreground reaches appPush.messages. Who shows a message with a title or a body depends on the platform:
| Android | iOS | |
|---|---|---|
| In the foreground | appPush shows it as a notification of the app, through appNotifications. | The system shows it, with the presentation options of the service. |
| In the background, or while the app is closed | The system shows it, in the channel and with the icon that the manifest names. | The system shows it. |
Either way, the user has to allow the notifications of the app, and a tap on the message reaches appPush.taps, not appNotifications.taps.
PushMessage.data is a Map<String, String>. A value of the data that is not a string, which iOS may give, becomes its JSON.
Presentation options on iOS
On iOS, firebase_messaging answers the system for every notification that arrives while the app is in the foreground, the notifications that the app shows itself with appNotifications too. Its presentation options are empty by default, and with them iOS would show none of these notifications in the foreground. So createFirebasePushService() sets them to an alert, a badge and a sound when the app starts:
PushService createFirebasePushService() {
final service = FirebasePushService(FirebaseMessaging.instance);
if (service.platformShowsInForeground) {
unawaited(
FirebaseMessaging.instance.setForegroundNotificationPresentationOptions(
alert: true,
badge: true,
sound: true,
),
);
}
return service;
}
platformShowsInForeground is true on iOS only. Keep the call, or the local notifications of the app stop showing in the foreground too.
Messages in the background
The app runs no code for a message that arrives in the background. A message with data and no title or body reaches the app only while it is in the foreground. To handle a message in the background, register a top-level function with FirebaseMessaging.onBackgroundMessage() in main(). It runs in an isolate of its own, without what bootstrap() sets up.
Images in a message do not show on iOS, which needs an extension of the app for them.
The permission
The service never asks for the permission to show notifications, and the code of the app does not call requestPermission() of the package either. The permission belongs to appNotifications.request() of the notifications role, and a message shows as a notification only once the user has allowed the notifications of the app. See the permission.
The channel and the icon on Android
The manifest names the channel and the small icon of the notifications of the app as those of Firebase Cloud Messaging:
<meta-data android:name="com.google.firebase.messaging.default_notification_channel_id" android:value="default"/>
<meta-data android:name="com.google.firebase.messaging.default_notification_icon" android:resource="@drawable/ic_notification"/>
Android shows a message that arrives in the background with them: in the channel default, which the module of notifications creates when the app starts, and with the small icon ic_notification of the notifications role. If you change the channel or the icon of the notifications, change these two lines too; see the channel and the small icon.
The APNs key
Firebase sends messages to iOS through APNs, with an APNs key of your team, which you upload to the Firebase project by hand, once. A key sends only to the apps of its team, so the App ID of the app has to be registered in that team, with Push Notifications. The key matters only for sending: the iOS app gets its address without it, but Firebase cannot send to that address.
Cloud Messaging has the steps, and how to send the device a test message from the Firebase console or a server. The README of the app lists the upload of the key and the App ID among the steps that you do by hand, under "Finish the setup", and the click-by-click guide shows each screen of both.
In tests
A test that runs main() or bootstrap() starts Firebase, which needs the options of flutterfire configure and a mock of its platform side. firebase_messaging has no platform side for tests either, so such a test answers the method channel plugins.flutter.io/firebase_messaging itself, with setMockMethodCallHandler of TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger. It also needs what a test of the notifications needs; see local_notifications. A test of a screen needs none of this: it gives appPush.start() a service of its own, which scripts what a provider answers; see Notifications.
Whichever module provides the push messages, the address is null until the service gives one and stays the same at the next start, each message in the foreground reaches appPush.messages once, a tap on a message reaches appPush.taps and not appNotifications.taps, and the provider never asks for the permission. SMF's CI checks this in running apps with the role. For this module, over a stand-in for the platform side that answers the messages of the plugin, it checks what the service sends to Firebase on Android and on iOS: the presentation options on iOS; never requestPermission; the registration token as the address; the first address on iOS once the device gets its APNs token; and the messages, the taps, the new tokens and the message that launched the app, with a value of the data that is not a string as its JSON. On devices, it checks that the address of the app, once it has one, is the registration token that the Firebase SDK has. On an Android emulator, it also sends the app with every module a push message from the Firebase project of CI, which has to reach appPush.messages with the title and the text that it was sent with. See Contributing and starting the apps.
Choosing it
smf create asks which module provides the push messages of the app, and offers None as well. That question comes before the one about the notifications. To choose the module without the question:
smf create my_app -m firebase_messaging
Firebase, the notifications and the preferences come with it, and smf create asks no question about the notifications or the preferences while one module provides each:
Adding firebase_core: a dependency of firebase_messaging.
Adding flutter_core: the only provider of the app entry role, which every app needs.
Adding local_notifications: the only provider of the notifications role, which firebase_messaging requires.
Adding shared_preferences: the only provider of the preferences role, which local_notifications requires.
After generation, firebase_core asks whether to run flutterfire configure, as for every app with Firebase; see Firebase. The module runs no command of its own: you upload the APNs key yourself, and the run ends with the link to the guide of that step by hand.