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
| Service | Free? | What It Does |
|---|---|---|
| Authentication | Yes (most providers) | User sign-in (email, Google, etc.) |
| Crashlytics | Yes | Automatic crash reporting |
| Analytics | Yes | Event tracking, user properties |
| Remote Config | Yes | Feature flags, A/B testing |
| Cloud Messaging (FCM) | Yes | Push notifications |
| App Check | Yes | Prevent unauthorized API access |
| Firestore | Free tier, then paid | Cloud database |
| Cloud Storage | Free tier, then paid | File storage |
| Performance Monitoring | Yes | App 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
- Go to console.firebase.google.com
- Click “Add project”
- Enter your project name
- Enable Google Analytics (recommended)
- 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:
- Go to Remote Config
- Click “Add parameter” with conditions
- Set different values for different user groups (e.g., 50% see value A, 50% see value B)
- 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
| Feature | Firebase | Supabase | Appwrite |
|---|---|---|---|
| Auth | Yes (free) | Yes (free) | Yes (free) |
| Crash reporting | Yes (free) | No | No |
| Analytics | Yes (free) | No | No |
| Database | Firestore (paid at scale) | Postgres (free tier) | Database (free tier) |
| Open source | No | Yes | Yes |
| Self-hostable | No | Yes | Yes |
| Vendor lock-in | High | Low | Low |
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.
Related Articles
- Android Tutorial #2: Hilt — injecting Firebase instances
- Jetpack Compose Tutorial #8: Navigation — auth flow navigation
- Android Tutorial #16: App Security — ProGuard mapping files
- Android Tutorial #11: Notifications — notification channels for FCM
This is part 17 of the Android Development Tutorial series.