Skip to content
All posts
March 23, 20264 min read

Android Push Notifications With FCM: The Complete Implementation Guide

Push notifications are one of the highest-retention tools available. Done wrong, they destroy engagement. Here's how to implement FCM notifications correctly in Android, handle notification permissions, and design a notification strategy that users actually want.

AndroidFirebaseKotlin
Share:

Push notifications have a bad reputation because most apps abuse them. "You haven't opened the app in 3 days" isn't a notification — it's harassment.

Done well, notifications provide genuine value at the right moment. Here's how to implement them correctly.


Setup: Firebase Cloud Messaging

Add FCM to your project:

kotlin
// build.gradle.kts
dependencies {
    implementation(platform("com.google.firebase:firebase-bom:33.0.0"))
    implementation("com.google.firebase:firebase-messaging")
}

Create a service to handle incoming messages:

kotlin
class TaskNotificationService : FirebaseMessagingService() {
    
    // Called when app is in foreground and a message arrives
    override fun onMessageReceived(message: RemoteMessage) {
        val title = message.notification?.title ?: message.data["title"] ?: return
        val body = message.notification?.body ?: message.data["body"] ?: return
        val taskId = message.data["task_id"]
        
        showNotification(title, body, taskId)
    }
    
    // Called when FCM token is refreshed — send new token to your server
    override fun onNewToken(token: String) {
        sendTokenToServer(token)
    }
    
    private fun showNotification(title: String, body: String, taskId: String?) {
        val intent = Intent(this, MainActivity::class.java).apply {
            flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
            taskId?.let { putExtra("task_id", it) }
        }
        
        val pendingIntent = PendingIntent.getActivity(
            this, 0, intent,
            PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
        )
        
        val notification = NotificationCompat.Builder(this, CHANNEL_ID)
            .setSmallIcon(R.drawable.ic_notification)
            .setContentTitle(title)
            .setContentText(body)
            .setPriority(NotificationCompat.PRIORITY_DEFAULT)
            .setAutoCancel(true)
            .setContentIntent(pendingIntent)
            .build()
        
        NotificationManagerCompat.from(this)
            .notify(System.currentTimeMillis().toInt(), notification)
    }
    
    companion object {
        const val CHANNEL_ID = "task_reminders"
    }
}

Register the service in

code
AndroidManifest.xml
:

xml
<service
    android:name=".notifications.TaskNotificationService"
    android:exported="false">
    <intent-filter>
        <action android:name="com.google.firebase.MESSAGING_EVENT" />
    </intent-filter>
</service>

Notification Channels (Required for Android 8+)

Create notification channels on app startup:

kotlin
class App : Application() {
    override fun onCreate() {
        super.onCreate()
        createNotificationChannels()
    }
    
    private fun createNotificationChannels() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val taskReminders = NotificationChannel(
                "task_reminders",
                "Task Reminders",
                NotificationManager.IMPORTANCE_DEFAULT
            ).apply {
                description = "Reminders for upcoming and overdue tasks"
                enableVibration(true)
            }
            
            val urgentAlerts = NotificationChannel(
                "urgent_alerts",
                "Urgent Alerts",
                NotificationManager.IMPORTANCE_HIGH
            ).apply {
                description = "Critical alerts requiring immediate attention"
            }
            
            val manager = getSystemService(NotificationManager::class.java)
            manager.createNotificationChannels(listOf(taskReminders, urgentAlerts))
        }
    }
}

Users can disable individual channels. Use specific channels for different notification types — users can silence low-priority channels without losing important ones.


Permission Handling (Android 13+)

Android 13 (API 33) requires explicit permission for notifications:

kotlin
class NotificationPermissionHandler(private val activity: AppCompatActivity) {
    
    private val requestPermissionLauncher = activity.registerForActivityResult(
        ActivityResultContracts.RequestPermission()
    ) { isGranted ->
        if (isGranted) {
            onPermissionGranted()
        } else {
            onPermissionDenied()
        }
    }
    
    fun requestIfNeeded() {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return
        
        when {
            ContextCompat.checkSelfPermission(
                activity, Manifest.permission.POST_NOTIFICATIONS
            ) == PackageManager.PERMISSION_GRANTED -> {
                // Already granted — nothing to do
            }
            activity.shouldShowRequestPermissionRationale(
                Manifest.permission.POST_NOTIFICATIONS
            ) -> {
                // Show explanation dialog first, then request
                showRationaleDialog()
            }
            else -> {
                requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
            }
        }
    }
    
    private fun showRationaleDialog() {
        MaterialAlertDialogBuilder(activity)
            .setTitle("Enable Reminders")
            .setMessage("Allow notifications to receive task reminders when they're due.")
            .setPositiveButton("Enable") { _, _ ->
                requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
            }
            .setNegativeButton("Not Now", null)
            .show()
    }
}

[!TIP] Request notification permission after demonstrating value — after the user creates their first task or sets a reminder. Cold permission requests (on first launch) have low acceptance rates.


Token Management

FCM tokens can change. Store and refresh them:

kotlin
class FCMTokenManager(
    private val userRepository: UserRepository,
    private val preferences: EncryptedSharedPreferences
) {
    
    suspend fun refreshToken() {
        try {
            val token = Firebase.messaging.getToken().await()
            val storedToken = preferences.getString("fcm_token", null)
            
            if (token != storedToken) {
                userRepository.updateFCMToken(token)
                preferences.edit().putString("fcm_token", token).apply()
            }
        } catch (e: Exception) {
            Timber.e(e, "Failed to refresh FCM token")
        }
    }
}

Call

code
refreshToken()
on app startup and when
code
onNewToken()
fires in the service.


Notification UX: What Not to Do

Don't send notifications for events that don't require user action. "Your weekly summary is ready" can wait for in-app discovery.

Don't use high-priority channels for medium-priority events. Crying wolf trains users to mute everything.

Don't send more than 1-2 notifications per day per user. The notification opt-out rate spikes above this frequency.

Do allow granular notification settings in-app:

kotlin
// Let users choose what they want
data class NotificationPreferences(
    val taskReminders: Boolean = true,
    val overdueAlerts: Boolean = true,
    val weeklyDigest: Boolean = false,
    val marketingUpdates: Boolean = false
)

Takeaways

  • Create separate notification channels for different notification types — users control visibility per channel
  • Request notification permission after demonstrating value, not on cold launch
  • Refresh and store FCM tokens — they expire and change
  • Design notifications for the user's benefit, not your engagement metrics
  • Allow granular opt-out in-app — users who opt out some notifications retain more than those who opt out all
Share:
S

Sudarshan Chaudhari

AI Systems Builder / Product Engineer

Bangkok, Thailand

Solo Android developer with 13+ years in QA, building Android apps, AI automation systems, and developer tools at SudarshanTechLabs.

Stay updated

Get new posts on Android, Kotlin, and solo dev straight to your inbox.

Newsletter preferences

Related Apps

MyFamilyTracker

Real-time family location sharing — Firebase Realtime DB for sub-second propagation, WorkManager + ForegroundService for OS-compliant background collection, geofencing via Google Maps API.

Building something? Available for Android dev and QA consulting.

Work with me

Comments — powered by Giscus

Apps tagged with this