Lesson 31 of 83 intermediate

Firebase on Android: Auth, FCM, Analytics, Crashlytics, Remote Config

Master Firebase's core pillars for production Android apps

Open interactive version (quiz + challenge)

Real-world analogy

Firebase is like a Swiss Army knife for your app's backend — Auth is the bouncer at the door, FCM is the postal service, Analytics is the CCTV system, Crashlytics is the incident report logger, and Remote Config is the light switches you can flip from HQ without touching the wiring.

What is it?

Firebase is Google's Backend-as-a-Service platform for mobile apps. Auth handles identity, FCM delivers push notifications, Analytics tracks user behavior, Crashlytics captures crash reports, and Remote Config enables server-driven feature flags — all with offline-first SDKs and tight Android integration.

Real-world relevance

In BRAC's enterprise field ops app, FCM data messages triggered WorkManager sync jobs when the server had new assignments. Crashlytics custom keys included the field officer's region and assignment ID so crashes were triaged by geography. Remote Config toggled an offline-first mode for areas with poor connectivity — no app update needed.

Key points

Code example

// 1. Firebase Auth — Google Sign-In
class AuthViewModel : ViewModel() {
    private val auth = FirebaseAuth.getInstance()

    fun signInWithGoogle(idToken: String) {
        val credential = GoogleAuthProvider.getCredential(idToken, null)
        auth.signInWithCredential(credential)
            .addOnSuccessListener { result ->
                val user = result.user ?: return@addOnSuccessListener
                // Refresh ID token for backend auth
                user.getIdToken(false).addOnSuccessListener { tokenResult ->
                    sendTokenToBackend(tokenResult.token!!)
                }
            }
            .addOnFailureListener { e ->
                FirebaseCrashlytics.getInstance().recordException(e)
            }
    }
}

// 2. FCM Service — handle all states
class AppMessagingService : FirebaseMessagingService() {

    override fun onNewToken(token: String) {
        // Upload to your server every time token rotates
        CoroutineScope(Dispatchers.IO).launch {
            runCatching { api.updateFcmToken(token) }
                .onFailure { saveTokenLocally(token) } // retry later
        }
    }

    override fun onMessageReceived(message: RemoteMessage) {
        // Notification payload in foreground — must show manually
        message.notification?.let { notif ->
            showNotification(notif.title, notif.body)
        }
        // Data payload — always arrives here regardless of app state
        message.data["sync_trigger"]?.let {
            enqueueSyncWork() // kick WorkManager
        }
    }
}

// 3. Crashlytics with context
fun processPayment(txnId: String, amount: Double) {
    val crashlytics = FirebaseCrashlytics.getInstance()
    crashlytics.setCustomKey("transaction_id", txnId)
    crashlytics.setCustomKey("amount", amount)
    crashlytics.log("Payment flow started")
    try {
        paymentGateway.charge(txnId, amount)
    } catch (e: Exception) {
        crashlytics.recordException(e) // non-fatal, won't crash app
        throw e
    }
}

// 4. Remote Config feature flag
val remoteConfig = Firebase.remoteConfig
remoteConfig.setDefaultsAsync(R.xml.remote_config_defaults)

suspend fun isNewCheckoutEnabled(): Boolean {
    remoteConfig.fetchAndActivate().await()
    return remoteConfig.getBoolean("enable_new_checkout_flow")
}

Line-by-line walkthrough

  1. 1. signInWithCredential(credential) exchanges the Google ID token for a Firebase session, giving you a FirebaseUser object.
  2. 2. user.getIdToken(false) retrieves a Firebase ID token (JWT) — pass this as a Bearer token to your own backend for authentication.
  3. 3. FirebaseCrashlytics.getInstance().recordException(e) logs the exception as non-fatal — it appears in Crashlytics under 'Non-fatals' without crashing the app.
  4. 4. onNewToken(token) is called on first install and every time the token rotates — upload it immediately; if offline, persist and retry via WorkManager.
  5. 5. onMessageReceived(message) is the single entry point for ALL FCM messages when the app is foregrounded, and for data-only messages in any app state.
  6. 6. message.notification?.let handles the notification payload — you must build and show the notification manually when foregrounded.
  7. 7. message.data["sync_trigger"] reads a custom data key — use this to trigger background work without displaying a notification.
  8. 8. remoteConfig.setDefaultsAsync(R.xml.remote_config_defaults) ensures your app always has fallback values before the first successful fetch.
  9. 9. remoteConfig.fetchAndActivate().await() atomically fetches and activates — values are only readable after activate().
  10. 10. remoteConfig.getBoolean("enable_new_checkout_flow") reads the flag — returns the in-app default if fetch hasn't completed yet.
  11. 11. crashlytics.setCustomKey(key, value) attaches metadata to every subsequent crash/non-fatal report in the same session.
  12. 12. crashlytics.log("Payment flow started") adds a breadcrumb visible in the Crashlytics console — trace the path that led to a crash.

Spot the bug

class AuthViewModel : ViewModel() {
    private val auth = FirebaseAuth.getInstance()

    fun getCurrentUserToken(): String {
        val user = auth.currentUser ?: return ""
        var token = ""
        user.getIdToken(false).addOnSuccessListener { result ->
            token = result.token ?: ""
        }
        return token  // Bug 1
    }

    fun signOut() {
        auth.signOut()  // Bug 2 — Google sign-in not cleared
    }
}

class AppMessagingService : FirebaseMessagingService() {
    override fun onMessageReceived(message: RemoteMessage) {
        message.notification?.let { notif ->
            showNotification(notif.title, notif.body)  // Bug 3
        }
    }
    // Bug 4 — missing override
}

fun setupRemoteConfig() {
    val remoteConfig = Firebase.remoteConfig
    // Bug 5 — no defaults set
    remoteConfig.fetchAndActivate().addOnSuccessListener {
        val flag = remoteConfig.getBoolean("new_feature")
        enableNewFeature(flag)
    }
}
Need a hint?
Look at token retrieval (async vs sync), sign-out completeness, foreground notification display conditions, a missing FirebaseMessagingService lifecycle override, and Remote Config defaults.
Show answer
Bug 1: getIdToken() is asynchronous — the success listener runs after return token, so token is always empty string. Fix: make getCurrentUserToken() a suspend function and use user.getIdToken(false).await().token ?: "". Bug 2: auth.signOut() only signs out of Firebase — it does NOT revoke the Google session. The next signInWithCredential will silently use the cached Google account without prompting. Fix: also call GoogleSignIn.getClient(context, options).signOut(). Bug 3: showNotification() is called unconditionally — but the FCM notification payload in foreground should only be shown manually; calling it also when the app is backgrounded would double-notify (the system already showed it). Add a foreground check: if (ProcessLifecycleOwner.get().lifecycle.currentState.isAtLeast(STARTED)) showNotification(...). Bug 4: onNewToken() is not overridden — when the FCM token rotates, the new token is never uploaded to the server. This causes all future push notifications to be delivered to the old (invalid) token. Fix: override onNewToken(token: String) and upload the token to your backend. Bug 5: No defaults are set before fetchAndActivate() — if the device is offline or the fetch quota is exceeded, getBoolean("new_feature") returns false (the SDK's internal default), which may silently disable a feature that should be on. Fix: call remoteConfig.setDefaultsAsync(R.xml.remote_config_defaults) before any fetch, ensuring your intended defaults are used when the server is unreachable.

Explain like I'm 5

Firebase is like a toolbox Google gives you for free. Auth is the ID checker at the door. FCM is the mailman that can deliver messages even when you're not home. Analytics is a notebook that secretly writes down what everyone does in your app. Crashlytics is a camera that takes a photo every time something breaks. Remote Config lets you change rules in the app without having to rebuild it — like a remote control.

Fun fact

FCM delivers over 700 billion messages per day globally — yet a single Android device can only hold one FCM token at a time. If a user logs into your app on two phones, the second login replaces the first token. Without multi-token management on the server side, you'll only notify one device.

Hands-on challenge

Build a FirebaseMessagingService that: (1) on onNewToken, saves the token to EncryptedSharedPreferences and enqueues a one-time WorkManager request to upload it with retry; (2) on onMessageReceived, if the data payload contains action=SYNC, enqueues a constrained WorkManager sync job; if it contains a notification payload and the app is foregrounded, displays a custom notification with a deep link PendingIntent; (3) attaches the current user UID as a Crashlytics custom key on every message received.

More resources

Open interactive version (quiz + challenge) ← Back to course: Android Interview Mastery