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.
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.
On this page
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.
Add FCM to your project:
// 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:
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
AndroidManifest.xml<service
android:name=".notifications.TaskNotificationService"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>Create notification channels on app startup:
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.
Android 13 (API 33) requires explicit permission for notifications:
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.
FCM tokens can change. Store and refresh them:
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
refreshToken()onNewToken()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:
// 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
)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.
Related Posts
Related Apps
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 meComments — powered by Giscus
Real-time family location sharing — Firebase Realtime DB for sub-second propagation, WorkManager + ForegroundService for OS-compliant background collection, geofencing via Google Maps API.
ReadPrivate dream journal — structured entry capture, pattern tagging, and optional Claude-powered insight generation. All data stays on-device by default.
ReadWorkout tracker — exercise logging with set/rep/weight history, goal progression, and local Room DB persistence. No account, no cloud sync required.
Read