androidengineers.Book a session

Designing a Messaging App

Notifications & FCM Delivery

article20 minMedium

Firebase Cloud Messaging (FCM) is the standard push notification service for Android. Getting notifications right means handling all delivery scenarios: app in foreground, background, and terminated state.

FCM Message Types

TypeDeliveredWakes apponMessageReceived called
Notification messageBy system if app backgroundedNoOnly if app is foreground
Data messageAlways to onMessageReceivedYes (high-priority)Yes
Notification + DataSystem handles notification; data in getExtras()NoForeground only

For apps that need full control, use data messages only and build your own notification.

Setup

// build.gradle.kts
implementation("com.google.firebase:firebase-messaging-ktx:23.4.1")
class AppMessagingService : FirebaseMessagingService() {

    // Called when a new FCM token is generated (first run or token refresh)
    override fun onNewToken(token: String) {
        // Send this token to your server so it can send push notifications
        CoroutineScope(Dispatchers.IO + SupervisorJob()).launch {
            serverApi.updateDeviceToken(token)
        }
    }

    // Called for ALL data messages; called for notification messages only when app is foreground
    override fun onMessageReceived(message: RemoteMessage) {
        val notificationType = message.data["type"] ?: return

        when (notificationType) {
            "new_message" -> handleNewMessage(message)
            "like" -> handleLikeNotification(message)
            "comment" -> handleCommentNotification(message)
        }
    }
}

Notification Channels (Required for API 26+)

fun createNotificationChannels(context: Context) {
    val notificationManager = context.getSystemService(NotificationManager::class.java)

    val channels = listOf(
        NotificationChannel(
            "messages",
            "Messages",
            NotificationManager.IMPORTANCE_HIGH
        ).apply {
            description = "New message notifications"
            enableVibration(true)
            setShowBadge(true)
        },
        NotificationChannel(
            "social",
            "Likes & Comments",
            NotificationManager.IMPORTANCE_DEFAULT
        ).apply {
            description = "Likes, comments, and follows"
        },
        NotificationChannel(
            "system",
            "System",
            NotificationManager.IMPORTANCE_LOW
        ).apply {
            description = "Account and security notifications"
        }
    )

    channels.forEach { notificationManager.createNotificationChannel(it) }
}

Building and Showing Notifications

private fun handleNewMessage(message: RemoteMessage) {
    val senderId = message.data["sender_id"] ?: return
    val senderName = message.data["sender_name"] ?: "Someone"
    val body = message.data["body"] ?: ""
    val conversationId = message.data["conversation_id"] ?: return

    // Deep link intent — opens the conversation when tapped
    val intent = Intent(this, MainActivity::class.java).apply {
        flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
        putExtra("destination", "conversation")
        putExtra("conversation_id", conversationId)
    }
    val pendingIntent = PendingIntent.getActivity(
        this, conversationId.hashCode(), intent,
        PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
    )

    val notification = NotificationCompat.Builder(this, "messages")
        .setSmallIcon(R.drawable.ic_notification)
        .setContentTitle(senderName)
        .setContentText(body)
        .setStyle(NotificationCompat.BigTextStyle().bigText(body))
        .setContentIntent(pendingIntent)
        .setAutoCancel(true)  // dismiss on tap
        .setPriority(NotificationCompat.PRIORITY_HIGH)
        .setCategory(NotificationCompat.CATEGORY_MESSAGE)
        .setShortcutId(senderId)  // link to conversation shortcut for People API
        .build()

    NotificationManagerCompat.from(this)
        .notify(conversationId.hashCode(), notification)
}

Notification Permission (API 33+)

class MainActivity : AppCompatActivity() {
    private val requestPermissionLauncher = registerForActivityResult(
        ActivityResultContracts.RequestPermission()
    ) { isGranted ->
        if (!isGranted) showRationale()
    }

    fun requestNotificationPermission() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
            when {
                ContextCompat.checkSelfPermission(
                    this, Manifest.permission.POST_NOTIFICATIONS
                ) == PackageManager.PERMISSION_GRANTED -> { /* already granted */ }

                shouldShowRequestPermissionRationale(Manifest.permission.POST_NOTIFICATIONS) ->
                    showRationale()

                else ->
                    requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
            }
        }
    }
}

Handling Deep Link from Notification

// In MainActivity.onCreate and onNewIntent:
override fun onNewIntent(intent: Intent) {
    super.onNewIntent(intent)
    handleNotificationDeepLink(intent)
}

private fun handleNotificationDeepLink(intent: Intent) {
    when (intent.getStringExtra("destination")) {
        "conversation" -> {
            val conversationId = intent.getStringExtra("conversation_id") ?: return
            navController.navigate("conversation/$conversationId")
        }
        "article" -> {
            val articleId = intent.getStringExtra("article_id") ?: return
            navController.navigate("article/$articleId")
        }
    }
}

Key Takeaways

ConceptRule
Data messagesUse for push; always triggers onMessageReceived regardless of app state
Notification channelsCreate all channels in Application.onCreate; never change channel ID after release
FLAG_IMMUTABLERequired for PendingIntent on API 31+
POST_NOTIFICATIONSRequest at runtime for API 33+; explain why before prompting
FCM token refreshAlways listen for onNewToken — tokens rotate; sync to server
Notification IDUse a meaningful ID (e.g., conversationId.hashCode) so updates replace, not stack

YOUR LEARNING JOURNEY

0 of 177 available lessons completed

Progress saved in this browser. No account needed.
Notifications & FCM Delivery | Android System Design | Android Engineers