๐Ÿ”Œ APIs, Databases, and Backend Integration

Firebase Cloud Messaging and Push Notifications

Push notifications bring users back to your app, when they're relevant. This guide sets up Firebase Cloud Messaging end to end: runtime permission, tokens, message types, channels, deep links, sending from a server with the HTTP v1 API, and testing.

โœ๏ธ Yunok Tech ๐Ÿ“… Updated โฑ 3 min read ๐ŸŽฏ Intermediate
On this page
  1. How FCM delivery works
  2. Setup
  3. Ask for permission at the right moment
  4. Notification channels
  5. Handling tokens and messages
  6. Notification vs data messages
  7. Sending from your server (HTTP v1)
  8. Testing
  9. Good notification use cases
  10. Common mistakes
  11. Frequently asked questions
  12. Conclusion

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

FCM token registration and message delivery The app obtains an FCM registration token and sends it to your backend. When an event happens, your backend calls the FCM HTTP v1 API with that token. FCM delivers the message to the device; the system shows notification messages automatically when the app is in background, or your FirebaseMessagingService handles data messages. Android appgets token Your backendstores token FCMHTTP v1 API 1. register token 2. send message 3. deliver to device โ†’ notification or onMessageReceived()
Your server decides who gets what. The app only manages its token and how messages are displayed.

Setup

app/build.gradle.ktsGradle KTS
dependencies {
    implementation(platform("com.google.firebase:firebase-bom:<latest>"))
    implementation("com.google.firebase:firebase-messaging")
}
AndroidManifest.xmlXML
<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.

NotificationPermission.kt (Compose)Kotlin
@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

Create channels at startupKotlin
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

AppMessagingService.ktKotlin
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 messageData message
App in backgroundSystem shows it automaticallyDelivered to onMessageReceived
App in foregroundonMessageReceivedonMessageReceived
Control over displayLimitedFull (channels, actions, grouping)
Best forSimple marketing or announcementsTransactional alerts, deep links, silent sync

Sending from your server (HTTP v1)

POST https://fcm.googleapis.com/v1/projects/PROJECT_ID/messages:sendJSON
{
  "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.

See all our apps โ†’

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.

Share: LinkedIn X / Twitter WhatsApp