Firebase gives your Android app superpowers. User authentication in minutes. Crash reports that show exactly what happened. Analytics that track user behavior. Feature flags that let you change app behavior without an update.

In this tutorial, you will integrate the most important Firebase services into your Android app — Authentication, Crashlytics, Analytics, Remote Config, Cloud Messaging, and App Check.

Prerequisites: You should have a working Android app with Hilt. See Android Tutorial #2: Hilt if needed.


Firebase Services Overview

ServiceFree?What It Does
AuthenticationYes (most providers)User sign-in (email, Google, etc.)
CrashlyticsYesAutomatic crash reporting
AnalyticsYesEvent tracking, user properties
Remote ConfigYesFeature flags, A/B testing
Cloud Messaging (FCM)YesPush notifications
App CheckYesPrevent unauthorized API access
FirestoreFree tier, then paidCloud database
Cloud StorageFree tier, then paidFile storage
Performance MonitoringYesApp performance tracking

For most apps, Authentication + Crashlytics + Analytics + Remote Config covers 80% of needs, and it is all free.


Setting Up Firebase

Step 1: Create a Firebase Project

  1. Go to console.firebase.google.com
  2. Click “Add project”
  3. Enter your project name
  4. Enable Google Analytics (recommended)
  5. Add an Android app with your package name

Step 2: Download google-services.json

Download google-services.json and put it in your app/ directory.

Important: Add google-services.json to your .gitignore. It contains API keys that should not be in your public repository.

Step 3: Add Dependencies

In your project-level build.gradle.kts:

plugins {
    id("com.google.gms.google-services") version "4.4.4" apply false
    id("com.google.firebase.crashlytics") version "3.0.6" apply false
}

In your app-level build.gradle.kts:

plugins {
    id("com.google.gms.google-services")
    id("com.google.firebase.crashlytics")
}

dependencies {
    // Firebase BoM — manages all Firebase versions
    implementation(platform("com.google.firebase:firebase-bom:34.10.0"))

    implementation("com.google.firebase:firebase-auth")
    implementation("com.google.firebase:firebase-crashlytics")
    implementation("com.google.firebase:firebase-analytics")
    implementation("com.google.firebase:firebase-config")
    implementation("com.google.firebase:firebase-config-ktx")  // Required for remoteConfigSettings { } extension
    implementation("com.google.firebase:firebase-messaging")
    implementation("com.google.firebase:firebase-appcheck-playintegrity")
}

The Firebase BoM (Bill of Materials) ensures all Firebase libraries use compatible versions.


Firebase Authentication

Email/Password Sign-Up

class AuthRepository @Inject constructor(
    private val auth: FirebaseAuth
) {
    val currentUser: Flow<FirebaseUser?> = callbackFlow {
        val listener = FirebaseAuth.AuthStateListener { auth ->
            trySend(auth.currentUser)
        }
        auth.addAuthStateListener(listener)
        awaitClose { auth.removeAuthStateListener(listener) }
    }

    suspend fun signUp(email: String, password: String): Result<FirebaseUser> {
        return try {
            val result = auth.createUserWithEmailAndPassword(email, password).await()
            Result.success(result.user!!)
        } catch (e: Exception) {
            Result.failure(e)
        }
    }

    suspend fun signIn(email: String, password: String): Result<FirebaseUser> {
        return try {
            val result = auth.signInWithEmailAndPassword(email, password).await()
            Result.success(result.user!!)
        } catch (e: Exception) {
            Result.failure(e)
        }
    }

    fun signOut() {
        auth.signOut()
    }
}

Providing Firebase with Hilt

@Module
@InstallIn(SingletonComponent::class)
object FirebaseModule {

    @Provides
    @Singleton
    fun provideFirebaseAuth(): FirebaseAuth = FirebaseAuth.getInstance()

    @Provides
    @Singleton
    fun provideFirebaseAnalytics(
        @ApplicationContext context: Context
    ): FirebaseAnalytics = FirebaseAnalytics.getInstance(context)

    @Provides
    @Singleton
    fun provideRemoteConfig(): FirebaseRemoteConfig =
        FirebaseRemoteConfig.getInstance()
}

Auth ViewModel

data class AuthState(
    val isLoading: Boolean = false,
    val user: FirebaseUser? = null,
    val error: String? = null
)

sealed interface AuthIntent {
    data class SignUp(val email: String, val password: String) : AuthIntent
    data class SignIn(val email: String, val password: String) : AuthIntent
    data object SignOut : AuthIntent
}

@HiltViewModel
class AuthViewModel @Inject constructor(
    private val authRepository: AuthRepository
) : ViewModel() {

    private val _state = MutableStateFlow(AuthState())
    val state: StateFlow<AuthState> = _state.asStateFlow()

    init {
        viewModelScope.launch {
            authRepository.currentUser.collect { user ->
                _state.update { it.copy(user = user) }
            }
        }
    }

    fun handleIntent(intent: AuthIntent) {
        when (intent) {
            is AuthIntent.SignUp -> signUp(intent.email, intent.password)
            is AuthIntent.SignIn -> signIn(intent.email, intent.password)
            is AuthIntent.SignOut -> authRepository.signOut()
        }
    }

    private fun signUp(email: String, password: String) {
        viewModelScope.launch {
            _state.update { it.copy(isLoading = true, error = null) }
            authRepository.signUp(email, password)
                .onSuccess { _state.update { it.copy(isLoading = false) } }
                .onFailure { e ->
                    _state.update {
                        it.copy(isLoading = false, error = e.message)
                    }
                }
        }
    }

    private fun signIn(email: String, password: String) {
        viewModelScope.launch {
            _state.update { it.copy(isLoading = true, error = null) }
            authRepository.signIn(email, password)
                .onSuccess { _state.update { it.copy(isLoading = false) } }
                .onFailure { e ->
                    _state.update {
                        it.copy(isLoading = false, error = e.message)
                    }
                }
        }
    }
}

Auth Screen

@Composable
fun AuthScreen(
    viewModel: AuthViewModel = hiltViewModel(),
    onAuthenticated: () -> Unit
) {
    val state by viewModel.state.collectAsStateWithLifecycle()

    LaunchedEffect(state.user) {
        if (state.user != null) {
            onAuthenticated()
        }
    }

    var email by remember { mutableStateOf("") }
    var password by remember { mutableStateOf("") }
    var isSignUp by remember { mutableStateOf(false) }

    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        Text(
            text = if (isSignUp) "Create Account" else "Sign In",
            style = MaterialTheme.typography.headlineMedium
        )

        Spacer(modifier = Modifier.height(24.dp))

        OutlinedTextField(
            value = email,
            onValueChange = { email = it },
            label = { Text("Email") },
            keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email),
            modifier = Modifier.fillMaxWidth()
        )

        Spacer(modifier = Modifier.height(8.dp))

        OutlinedTextField(
            value = password,
            onValueChange = { password = it },
            label = { Text("Password") },
            visualTransformation = PasswordVisualTransformation(),
            modifier = Modifier.fillMaxWidth()
        )

        state.error?.let { error ->
            Spacer(modifier = Modifier.height(8.dp))
            Text(
                text = error,
                color = MaterialTheme.colorScheme.error,
                style = MaterialTheme.typography.bodySmall
            )
        }

        Spacer(modifier = Modifier.height(16.dp))

        Button(
            onClick = {
                val intent = if (isSignUp) {
                    AuthIntent.SignUp(email, password)
                } else {
                    AuthIntent.SignIn(email, password)
                }
                viewModel.handleIntent(intent)
            },
            modifier = Modifier.fillMaxWidth(),
            enabled = !state.isLoading
        ) {
            if (state.isLoading) {
                CircularProgressIndicator(
                    modifier = Modifier.size(20.dp),
                    strokeWidth = 2.dp
                )
            } else {
                Text(if (isSignUp) "Sign Up" else "Sign In")
            }
        }

        TextButton(onClick = { isSignUp = !isSignUp }) {
            Text(
                if (isSignUp) "Already have an account? Sign In"
                else "Don't have an account? Sign Up"
            )
        }
    }
}

Google Sign-In

class GoogleSignInHelper @Inject constructor(
    private val auth: FirebaseAuth,
    @ApplicationContext private val context: Context
) {
    suspend fun signInWithCredential(idToken: String): Result<FirebaseUser> {
        return try {
            val credential = GoogleAuthProvider.getCredential(idToken, null)
            val result = auth.signInWithCredential(credential).await()
            Result.success(result.user!!)
        } catch (e: Exception) {
            Result.failure(e)
        }
    }
}

Firebase Crashlytics

Crashlytics reports crashes automatically. No code needed for basic setup — just add the dependency and plugin.

Custom Crash Keys

Add context to crash reports:

class CrashlyticsHelper @Inject constructor() {

    fun setUser(userId: String) {
        Firebase.crashlytics.setUserId(userId)
    }

    fun setCustomKey(key: String, value: String) {
        Firebase.crashlytics.setCustomKey(key, value)
    }

    fun log(message: String) {
        Firebase.crashlytics.log(message)
    }

    fun recordException(throwable: Throwable) {
        Firebase.crashlytics.recordException(throwable)
    }
}

Use it in your repository:

class UserRepository @Inject constructor(
    private val api: UserApi,
    private val crashlytics: CrashlyticsHelper
) {
    suspend fun getUser(id: String): User? {
        return try {
            crashlytics.log("Fetching user $id")
            api.getUser(id)
        } catch (e: Exception) {
            crashlytics.recordException(e)
            crashlytics.setCustomKey("failed_user_id", id)
            null
        }
    }
}

ProGuard Mapping Files

For readable crash reports with R8 enabled, upload the mapping file:

// In build.gradle.kts
android {
    buildTypes {
        release {
            // Crashlytics automatically uploads mapping files
            // when the plugin is applied
        }
    }
}

The Crashlytics Gradle plugin handles this automatically. See Android Tutorial #16: App Security for more on R8 and mapping files.


Firebase Analytics

Track user events and behavior:

class AnalyticsHelper @Inject constructor(
    private val analytics: FirebaseAnalytics
) {
    fun logScreenView(screenName: String) {
        analytics.logEvent(FirebaseAnalytics.Event.SCREEN_VIEW) {
            param(FirebaseAnalytics.Param.SCREEN_NAME, screenName)
        }
    }

    fun logEvent(name: String, params: Map<String, String> = emptyMap()) {
        analytics.logEvent(name) {
            params.forEach { (key, value) ->
                param(key, value)
            }
        }
    }

    fun logPurchase(itemName: String, price: Double) {
        analytics.logEvent(FirebaseAnalytics.Event.PURCHASE) {
            param(FirebaseAnalytics.Param.ITEM_NAME, itemName)
            param(FirebaseAnalytics.Param.PRICE, price)
            param(FirebaseAnalytics.Param.CURRENCY, "USD")
        }
    }

    fun setUserProperty(key: String, value: String) {
        analytics.setUserProperty(key, value)
    }
}

Track screen views automatically in your NavHost:

@Composable
fun AppNavigation(
    navController: NavHostController,
    analyticsHelper: AnalyticsHelper
) {
    LaunchedEffect(navController) {
        navController.currentBackStackEntryFlow.collect { entry ->
            val route = entry.destination.route
            if (route != null) {
                analyticsHelper.logScreenView(route)
            }
        }
    }

    NavHost(navController = navController, startDestination = HomeRoute) {
        // Navigation graph
    }
}

Firebase Remote Config

Remote Config lets you change app behavior without releasing an update. Perfect for feature flags and A/B testing.

Setup

class RemoteConfigRepository @Inject constructor(
    private val remoteConfig: FirebaseRemoteConfig
) {
    init {
        val configSettings = remoteConfigSettings {
            minimumFetchIntervalInSeconds = if (BuildConfig.DEBUG) 0 else 3600
        }
        remoteConfig.setConfigSettingsAsync(configSettings)
        remoteConfig.setDefaultsAsync(R.xml.remote_config_defaults)
    }

    suspend fun fetchAndActivate(): Boolean {
        return try {
            remoteConfig.fetchAndActivate().await()
        } catch (e: Exception) {
            false
        }
    }

    fun getBoolean(key: String): Boolean = remoteConfig.getBoolean(key)
    fun getString(key: String): String = remoteConfig.getString(key)
    fun getLong(key: String): Long = remoteConfig.getLong(key)
}

Default Values

Create res/xml/remote_config_defaults.xml:

<?xml version="1.0" encoding="utf-8"?>
<defaultsMap>
    <entry>
        <key>show_premium_banner</key>
        <value>false</value>
    </entry>
    <entry>
        <key>max_free_items</key>
        <value>10</value>
    </entry>
    <entry>
        <key>welcome_message</key>
        <value>Welcome to the app!</value>
    </entry>
</defaultsMap>

Using Feature Flags

@HiltViewModel
class HomeViewModel @Inject constructor(
    private val remoteConfig: RemoteConfigRepository
) : ViewModel() {

    val showPremiumBanner: Boolean
        get() = remoteConfig.getBoolean("show_premium_banner")

    val welcomeMessage: String
        get() = remoteConfig.getString("welcome_message")

    init {
        viewModelScope.launch {
            remoteConfig.fetchAndActivate()
        }
    }
}

A/B Testing

In the Firebase Console:

  1. Go to Remote Config
  2. Click “Add parameter” with conditions
  3. Set different values for different user groups (e.g., 50% see value A, 50% see value B)
  4. Use Analytics to measure which variant performs better

Firebase Cloud Messaging (FCM)

Send push notifications to your users:

class MyFirebaseMessagingService : FirebaseMessagingService() {

    override fun onNewToken(token: String) {
        // Send token to your server
        sendTokenToServer(token)
    }

    override fun onMessageReceived(remoteMessage: RemoteMessage) {
        val title = remoteMessage.notification?.title ?: "Notification"
        val body = remoteMessage.notification?.body ?: ""
        val data = remoteMessage.data

        showNotification(title, body, data)
    }

    private fun showNotification(
        title: String,
        body: String,
        data: Map<String, String>
    ) {
        val deepLink = data["deep_link"]

        val intent = if (deepLink != null) {
            Intent(Intent.ACTION_VIEW, deepLink.toUri()).apply {
                setPackage(packageName)
            }
        } else {
            Intent(this, MainActivity::class.java)
        }

        val pendingIntent = PendingIntent.getActivity(
            this, 0, intent,
            PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
        )

        val notification = NotificationCompat.Builder(this, "messages")
            .setSmallIcon(R.drawable.ic_notification)
            .setContentTitle(title)
            .setContentText(body)
            .setContentIntent(pendingIntent)
            .setAutoCancel(true)
            .build()

        NotificationManagerCompat.from(this)
            .notify(System.currentTimeMillis().toInt(), notification)
    }

    private fun sendTokenToServer(token: String) {
        // Send to your backend
    }
}

Declare in the manifest:

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

Firebase App Check

App Check verifies that requests come from your genuine app, not from scripts or modified APKs:

class MyApp : Application() {
    override fun onCreate() {
        super.onCreate()

        val firebaseAppCheck = FirebaseAppCheck.getInstance()
        firebaseAppCheck.installAppCheckProviderFactory(
            PlayIntegrityAppCheckProviderFactory.getInstance()
        )
    }
}

For debug builds:

if (BuildConfig.DEBUG) {
    firebaseAppCheck.installAppCheckProviderFactory(
        DebugAppCheckProviderFactory.getInstance()
    )
}

Firebase vs Alternatives

FeatureFirebaseSupabaseAppwrite
AuthYes (free)Yes (free)Yes (free)
Crash reportingYes (free)NoNo
AnalyticsYes (free)NoNo
DatabaseFirestore (paid at scale)Postgres (free tier)Database (free tier)
Open sourceNoYesYes
Self-hostableNoYesYes
Vendor lock-inHighLowLow

Choose Firebase when you want the fastest setup and don’t mind vendor lock-in. The free tier covers most indie apps.

Choose Supabase/Appwrite when you want full control, open source, or need to avoid Google services.


Common Mistakes

1. Not Setting Up Crashlytics ProGuard Rules

Without mapping files, crash reports show obfuscated class names. The Crashlytics Gradle plugin handles this automatically, but verify it works by triggering a test crash.

2. Committing google-services.json

This file contains your Firebase API keys. While they are restricted to your app’s package name, it is still best practice to keep them out of public repos.

3. Fetching Remote Config on Every Screen

Remote Config has rate limits. Fetch once at app startup and use cached values. The minimumFetchIntervalInSeconds setting prevents excessive fetches.

4. Not Handling Auth State Changes

When a user signs out or their token expires, the auth state changes. Use AuthStateListener (as shown above) to react to these changes instead of checking once.

5. Logging Too Many Analytics Events

Firebase Analytics has limits: 500 distinct event types, 25 parameters per event. Focus on events that matter for your business decisions.


What’s Next?

In this tutorial, you learned:

  • Setting up Firebase with the BoM
  • Email/password and Google Sign-In authentication
  • Automatic crash reporting with Crashlytics
  • Tracking events and screen views with Analytics
  • Feature flags and A/B testing with Remote Config
  • Push notifications with FCM
  • App verification with App Check

Next up: Android Tutorial #18: CI/CD for Android — GitHub Actions + Fastlane — where you will learn how to automate building, testing, and deploying your app.



This is part 17 of the Android Development Tutorial series.