How it works
The explanation, with examples you can run.
Tokens
class PushService : FirebaseMessagingService() { override fun onNewToken(token: String) { // A new install, a restored backup, or a token rotation: tell your server. enqueueTokenUpload(token) // via WorkManager, so it survives being offline } override fun onMessageReceived(message: RemoteMessage) { val orderId = message.data["orderId"] ?: return if (message.data["kind"] == "sync") { enqueueSync(orderId) // anything slow goes to WorkManager } else { showOrderNotification(orderId, message.data["title"].orEmpty()) } }}Tokens change: on reinstall, when app data is cleared, when restored to a new device, and occasionally on their own. Store them with a timestamp, refresh from the app now and then (FirebaseMessaging.getInstance().token), and remove tokens your sends report as unregistered. Firebase treats devices that haven't connected for over a month as stale; sending to them wastes quota and distorts delivery numbers.
Two message types
- Notification messages have a
notificationblock (title, body, image). When the app is in the background, the FCM SDK posts the notification itself, and tapping it opens your launcher activity. When the app is in the foreground,onMessageReceived()gets it and you decide what to show. - Data messages carry only your key-value pairs and always go to
onMessageReceived()(subject to delivery and priority rules). They give you full control, and full responsibility: you build the notification.
Payloads are limited to 4096 bytes. Send ids, not documents, and fetch the rest.
| App state | Notification | Data | Both |
|---|---|---|---|
| Foreground | onMessageReceived | onMessageReceived | onMessageReceived |
| Background | System tray | onMessageReceived | Tray, data in the launch intent's extras |
Priority and Doze
Normal priority (the default) is delivered straight away when the device is awake, and may wait for a maintenance window in Doze. High priority wakes the device, gives the app a short window of processing with limited network access, and allows it to start a foreground service from the background. FCM watches the last 7 days of behaviour per app instance: if high-priority messages don't result in user-visible notifications, later ones may be downgraded to normal. Use high priority for chats and calls, normal for "sync your inbox".
A force-stopped app (from Settings) receives nothing until the user opens it again.
The user's side
On Android 13+, notifications need the POST_NOTIFICATIONS runtime permission; a push that arrives without it is received but not shown. Create notification channels for each kind of message (orders, chat, marketing) so users can mute one without losing the others, and include an idempotency key in the data: messages can be delivered more than once.
Check yourself
3 questions. Get them all right to finish this topic.
0 of 3 right so far. Get all three to finish this topic.