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
- Firebase Auth flows — Email/password, Google Sign-In (ActivityResultContracts + GoogleSignInClient), and phone OTP (PhoneAuthProvider). Always use FirebaseAuth.getInstance() and observe currentUser on resume, not just on sign-in.
- ID Token vs UID — UID is a stable user identifier; the ID token is a short-lived JWT (1 hour) for authenticating requests to your backend. Call user.getIdToken(forceRefresh) before every authenticated API call to avoid 401s.
- FCM message types — Notification messages are handled by the system when the app is in the background/killed — tray entry appears automatically. Data messages are always delivered to onMessageReceived() in FirebaseMessagingService. A message can contain both payloads.
- FCM in foreground — When the app is foregrounded, notification payload is NOT shown automatically — you must build and display the notification manually inside onMessageReceived(). Data payload always arrives here regardless of app state.
- FCM token lifecycle — Token is generated on first launch and can rotate (app reinstall, token expiry, user clears data). Override onNewToken() in FirebaseMessagingService and upload the new token to your server immediately. In BRAC field ops, stale tokens caused missed sync notifications.
- Analytics event logging — FirebaseAnalytics.getInstance(context).logEvent(FirebaseAnalytics.Event.SELECT_ITEM, bundle). Use predefined event names where possible (SELECT_ITEM, LOGIN, PURCHASE) for automatic dashboard integration. Custom events are also fully supported.
- Analytics user properties — setUserProperty(name, value) segments users in dashboards. In Tixio, user_plan and workspace_size properties drove funnel analysis. Properties persist across sessions; set them once on login, update on plan change.
- Crashlytics setup — Add google-services plugin, apply com.google.firebase.crashlytics plugin, and initialize automatically via manifest provider. For non-fatal errors: FirebaseCrashlytics.getInstance().recordException(e). For breadcrumbs: log("step description").
- Crashlytics custom keys — setCustomKey(key, value) attaches metadata to every crash report. In Payback fintech, we set user_id, transaction_id, and screen_name so crashes were immediately actionable without reproducing blindly.
- Remote Config basics — Define defaults in XML (res/xml/remote_config_defaults.xml), fetch with remoteConfig.fetchAndActivate(), then read values with getString/getBoolean/getLong. Always set in-app defaults so the app works offline or before first fetch.
- Remote Config fetch interval — Default minimum fetch interval is 12 hours in production to prevent quota exhaustion. During development use setMinimumFetchIntervalInSeconds(0). Never rely on immediate propagation — remote config is eventually consistent.
- Feature flags with Remote Config — Boolean flags like enable_new_checkout_flow let you roll out features to 1% of users, test in production, and kill-switch instantly without a Play Store update. In Tixio SaaS, this replaced multiple beta tracks.
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. signInWithCredential(credential) exchanges the Google ID token for a Firebase session, giving you a FirebaseUser object.
- 2. user.getIdToken(false) retrieves a Firebase ID token (JWT) — pass this as a Bearer token to your own backend for authentication.
- 3. FirebaseCrashlytics.getInstance().recordException(e) logs the exception as non-fatal — it appears in Crashlytics under 'Non-fatals' without crashing the app.
- 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. 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. message.notification?.let handles the notification payload — you must build and show the notification manually when foregrounded.
- 7. message.data["sync_trigger"] reads a custom data key — use this to trigger background work without displaying a notification.
- 8. remoteConfig.setDefaultsAsync(R.xml.remote_config_defaults) ensures your app always has fallback values before the first successful fetch.
- 9. remoteConfig.fetchAndActivate().await() atomically fetches and activates — values are only readable after activate().
- 10. remoteConfig.getBoolean("enable_new_checkout_flow") reads the flag — returns the in-app default if fetch hasn't completed yet.
- 11. crashlytics.setCustomKey(key, value) attaches metadata to every subsequent crash/non-fatal report in the same session.
- 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
- Firebase Auth Android Guide (Firebase Docs)
- FCM Android Setup & Message Handling (Firebase Docs)
- Crashlytics Android Integration (Firebase Docs)
- Remote Config Android Quickstart (Firebase Docs)
- Firebase Analytics Best Practices for Android (Firebase Docs)