On this page
Notifications are a privilege, not a channel you own. Since Android 13, users must explicitly grant permission, and they revoke it quickly when apps spam. This guide shows how to implement FCM correctly and how to ask for permission at a moment that makes sense.
How FCM delivery works
Setup
dependencies {
implementation(platform("com.google.firebase:firebase-bom:<latest>"))
implementation("com.google.firebase:firebase-messaging")
}<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application>
<service
android:name=".push.AppMessagingService"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
<meta-data
android:name="com.google.firebase.messaging.default_notification_channel_id"
android:value="general" />
</application>Ask for permission at the right moment
On Android 13 (API 33) and later, POST_NOTIFICATIONS is a runtime permission. Ask in context, for example after the user places an order ("Get delivery updates?"), not on first launch.
@Composable
fun rememberNotificationPermissionRequester(onResult: (Boolean) -> Unit): () -> Unit {
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission(), onResult)
return remember {
{
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
launcher.launch(Manifest.permission.POST_NOTIFICATIONS)
} else onResult(true) // granted by default below Android 13 (user can still disable)
}
}
}Notification channels
fun createNotificationChannels(context: Context) {
val manager = context.getSystemService(NotificationManager::class.java)
manager.createNotificationChannels(listOf(
NotificationChannel("orders", "Order updates", NotificationManager.IMPORTANCE_HIGH)
.apply { description = "Shipping and delivery status" },
NotificationChannel("general", "General", NotificationManager.IMPORTANCE_DEFAULT),
NotificationChannel("promotions", "Offers", NotificationManager.IMPORTANCE_LOW),
))
}Users can turn off "Offers" while keeping "Order updates". Separate channels respect their choice and keep important alerts alive.
Handling tokens and messages
class AppMessagingService : FirebaseMessagingService() {
override fun onNewToken(token: String) {
// Tokens can change (reinstall, data cleared). Send to backend via WorkManager for reliability.
TokenUploadWorker.enqueue(applicationContext, token)
}
override fun onMessageReceived(message: RemoteMessage) {
// Data messages arrive here in foreground and background.
// Notification messages arrive here only when the app is in the foreground.
val data = message.data
when (data["type"]) {
"order_update" -> showOrderNotification(
orderId = data["orderId"] ?: return,
title = data["title"] ?: "Order update",
body = data["body"].orEmpty(),
)
"sync" -> SyncScheduler(WorkManager.getInstance(this)).requestSync() // silent refresh
}
}
private fun showOrderNotification(orderId: String, title: String, body: String) {
val intent = Intent(Intent.ACTION_VIEW, "https://yunoktech.com/orders/$orderId".toUri(), this, MainActivity::class.java)
val pending = PendingIntent.getActivity(this, orderId.hashCode(), intent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT)
val notification = NotificationCompat.Builder(this, "orders")
.setSmallIcon(R.drawable.ic_stat_notify)
.setContentTitle(title)
.setContentText(body)
.setAutoCancel(true)
.setContentIntent(pending)
.build()
if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED ||
Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
NotificationManagerCompat.from(this).notify(orderId.hashCode(), notification)
}
}
}Notification vs data messages
| Notification message | Data message | |
|---|---|---|
| App in background | System shows it automatically | Delivered to onMessageReceived |
| App in foreground | onMessageReceived | onMessageReceived |
| Control over display | Limited | Full (channels, actions, grouping) |
| Best for | Simple marketing or announcements | Transactional alerts, deep links, silent sync |
Sending from your server (HTTP v1)
{
"message": {
"token": "DEVICE_REGISTRATION_TOKEN",
"data": {
"type": "order_update",
"orderId": "A1042",
"title": "Out for delivery",
"body": "Your order arrives today by 6 PM"
},
"android": { "priority": "high" }
}
}The legacy FCM server APIs have been retired. Use HTTP v1 with OAuth 2.0 service-account credentials, which must stay on your server and never ship in the app. The Firebase Admin SDK handles authentication for you.
Testing
- Use the Firebase console's "Send test message" with your device token.
- Test foreground, background and killed states. They behave differently.
- Test with notifications disabled, and with a specific channel disabled.
- Check behaviour on manufacturers with aggressive battery management.
Good notification use cases
Transactional
Order shipped, payment received, OTP alternatives. High value, expected by users.
Collaboration
Mentions, replies and shared-document changes, grouped and rate-limited.
Silent sync
A data message triggers WorkManager to refresh content before the user opens the app.
Common mistakes
- Requesting notification permission on first launch with no context.
- Sending everything through one channel, so users disable all notifications.
- Heavy work in
onMessageReceived. Enqueue WorkManager instead. - Putting personal or sensitive data in notification text on the lock screen.
- Not handling token refresh, so pushes silently stop reaching users.
Frequently asked questions
Why don't my notifications arrive on some phones?
Common causes are denied permission, a disabled channel, aggressive OEM battery optimisation for background apps, or an invalid token on the server. Check delivery reports in Firebase.
Can I send to groups of users?
Yes, with topics (subscribeToTopic) or by sending to multiple tokens from your server.
Is FCM free?
FCM itself is free to use. Your backend infrastructure for sending messages has its own costs.
Conclusion
A solid FCM integration asks for permission in context, uses meaningful channels, prefers data messages for transactional alerts with deep links, keeps tokens in sync with your server, and sends with the HTTP v1 API from secure server code.
Next step: compare real-time options in WebSockets vs REST APIs in Android.
Official references
๐ฑ Apps built by Yunok Tech
Real Android apps on Google Play, free to download.
NotiVaultNotification history: recover dismissed notificationsGet it โธ
Walzi4K wallpapers for lock and home screenGet it โธ
Ready to build real Android applications?
Networking, persistence and offline sync are where real apps get complicated. Practise building a data layer that handles slow networks, failures and caching, not just the happy path.
Move beyond theory and strengthen your Android development skills with Yunoktech's Android course. Learn core concepts, practical implementation, modern Android development techniques, and the reasoning behind building maintainable applications.
Explore the Android course and take your next step as an Android developer.