Droid Manual // Android Knowledge Base

Droid Manual

Firebase Integration Tutorial

October 30, 2026 · 9 min read

Firebase is Google's mobile platform that simplifies backend development for Android apps. It provides ready-made services like authentication, real-time databases, file storage, and analytics without managing servers. In this tutorial, you will learn to integrate Firebase into an Android project using Kotlin and Jetpack Compose. We will cover the full lifecycle from project setup to implementing secure data operations with MVVM architecture.

Setting Up the Firebase Project

Before touching Android Studio, you must create a Firebase project in the Firebase console. Navigate to console.firebase.google.com, sign in with a Google account, and click "Add project." Give your project a name, enable Google Analytics if needed, and complete the wizard. Once created, you will see a dashboard with various Firebase services. Click the Android icon to register your app. You must enter your app's package name (found in your build.gradle.kts) exactly as it appears. Download the generated google-services.json file and place it in your app module directory. This JSON file contains your Firebase project's configuration keys and connects your app to the correct backend.

After placing the configuration file, you need to add the Google Services plugin to your Gradle files. Open your project-level build.gradle.kts and add the classpath dependency for the Google Services plugin. Then, in your app-level build.gradle.kts, apply the plugin and add the Firebase BOM (Bill of Materials) to manage dependency versions consistently. The BOM ensures all Firebase libraries use compatible versions.

// Project-level build.gradle.kts
plugins {
    id("com.android.application") version "8.2.0" apply false
    id("org.jetbrains.kotlin.android") version "1.9.20" apply false
    id("com.google.gms.google-services") version "4.4.0" apply false
}

The code above registers the Google Services plugin at the project level. This allows Android Studio to recognize the google-services.json file and process it during the build. Without this step, Firebase initialization will fail silently or throw an exception at runtime. Next, apply the plugin in your app module.

// App-level build.gradle.kts
plugins {
    id("com.android.application")
    id("org.jetbrains.kotlin.android")
    id("com.google.gms.google-services")
}

dependencies {
    // Firebase BOM
    implementation(platform("com.google.firebase:firebase-bom:32.7.0"))
    // Firebase Authentication
    implementation("com.google.firebase:firebase-auth-ktx")
    // Cloud Firestore
    implementation("com.google.firebase:firebase-firestore-ktx")
    // Firebase Storage
    implementation("com.google.firebase:firebase-storage-ktx")
}

This configuration imports the Firebase BOM and specific modules for authentication, Firestore, and Storage. The KTX suffix indicates Kotlin extensions that provide coroutine-friendly APIs. When you sync the project, Gradle downloads the necessary libraries and merges the google-services.json metadata into your app's resources.

Authentication with Firebase

Authentication is often the first feature to implement. Firebase Auth supports email/password, Google Sign-In, and numerous other providers. For this tutorial, we focus on email/password authentication because it demonstrates the core workflow. You must enable the desired sign-in method in the Firebase console under Authentication > Sign-in method. Create a new project or select an existing one, navigate to the Sign-in method tab, and enable Email/Password.

In your Android app, you can handle authentication using Kotlin coroutines. Create a ViewModel that exposes state for the authentication result. Use the FirebaseAuth instance to register or log in users. The createUserWithEmailAndPassword and signInWithEmailAndPassword functions return Task objects that can be collected via coroutines. This approach keeps your UI thread free while network requests occur.

class AuthViewModel : ViewModel() {
    private val auth: FirebaseAuth = Firebase.auth
    private val _authState = MutableStateFlow<AuthState>(AuthState.Loading)
    val authState: StateFlow<AuthState> = _authState.asStateFlow()

    fun signIn(email: String, password: String) {
        viewModelScope.launch {
            try {
                val task = auth.signInWithEmailAndPassword(email, password).await()
                if (task.user != null) {
                    _authState.value = AuthState.Success(task.user!!)
                }
            } catch (e: Exception) {
                _authState.value = AuthState.Error(e.message ?: "Unknown error")
            }
        }
    }
}

sealed class AuthState {
    object Loading : AuthState()
    data class Success(val user: FirebaseUser) : AuthState()
    data class Error(val message: String) : AuthState()
}

The ViewModel wraps Firebase Auth operations in a coroutine scope. The await() extension converts the Task into a suspend function, allowing seamless integration with structured concurrency. If the sign-in succeeds, the view model emits a Success state containing the user object. If an error occurs, such as invalid credentials, it emits an Error state with a descriptive message. Your Compose UI can then collect this flow and display appropriate feedback.

Cloud Firestore Data Operations

Cloud Firestore is a NoSQL document database that stores data in collections and documents. It provides real-time listeners and offline persistence out of the box. To use Firestore, you must define your data structure. For example, a users collection could contain documents with fields like email, displayName, and createdAt.

In your repository layer, create a data class representing your entity. Use the FirebaseFirestore instance to read and write data. Firestore's Kotlin extensions allow you to convert documents to Kotlin objects using toObject(). You can add, update, or delete documents with simple function calls. Remember that Firestore operations are asynchronous, so always handle them within coroutines.

data class User(
    val uid: String = "",
    val email: String = "",
    val displayName: String = "",
    val createdAt: Long = System.currentTimeMillis()
)

class FirestoreRepository @Inject constructor() {
    private val db: FirebaseFirestore = Firebase.firestore

    suspend fun addUser(user: User) {
        db.collection("users").document(user.uid).set(user)
    }

    suspend fun getUser(uid: String): User? {
        return db.collection("users").document(uid).get().await().toObject(User::class.java)
    }

    fun observeUsers(): Flow<List<User>> {
        return db.collection("users").snapshots().map { snapshot ->
            snapshot.documents.mapNotNull { it.toObject(User::class.java) }
        }
    }
}

The repository pattern separates data logic from your UI. The addUser function sets a document with the user's UID as the document ID. The getUser function retrieves a single document and maps it to a Kotlin object. The observeUsers function returns a Flow that emits lists of users whenever the collection changes. This reactive approach enables your Compose UI to update automatically when data changes in Firestore.

Firebase Storage for Files

Firebase Storage allows you to upload and download files like images, videos, and documents. It integrates seamlessly with Firestore, so you can store download URLs in your database. To upload a file, you need a StorageReference pointing to a path in your bucket. You can upload bytes, streams, or file paths. The upload process returns a Task<UploadTask.TaskSnapshot> that you can await for completion.

For this tutorial, we will implement a simple image upload feature. Create a function that takes an ImageBitmap or byte array, generates a unique filename, and uploads it to storage. After the upload completes, retrieve the download URL and save it to Firestore. This two-step process ensures your database holds the reference while the file resides in storage.

class StorageRepository @Inject constructor() {
    private val storage: FirebaseStorage = Firebase.storage
    private val db: FirebaseFirestore = Firebase.firestore

    suspend fun uploadImage(userId: String, bitmap: Bitmap): String {
        val filename = "avatars/$userId/${UUID.randomUUID()}.jpg"
        val ref = storage.reference.child(filename)
        
        val baos = ByteArrayOutputStream()
        bitmap.compress(Bitmap.CompressFormat.JPEG, 80, baos)
        val data = baos.toByteArray()
        
        ref.putBytes(data).await()
        return ref.downloadUrl.await().toString()
    }

    suspend fun saveAvatarUrl(userId: String, url: String) {
        db.collection("users").document(userId)
            .update("avatarUrl", url)
            .await()
    }
}

The code compresses the bitmap to JPEG format with 80% quality to reduce file size. It then uploads the bytes to a structured path in Firebase Storage. The await() calls ensure the function completes before returning the download URL. This pattern keeps your storage operations organized and reusable across your application.

Security Rules and Best Practices

Firebase Security Rules control who can read and write your data. They are essential for protecting user information and preventing unauthorized access. Firestore rules are written in a domain-specific language and deployed to the Firebase console. A common pattern is to restrict access based on the authenticated user's UID. For example, you can allow users to read and write only their own documents.

Storage rules similarly control file access. You can require authentication for all uploads and restrict access to specific paths. Always validate that the request comes from an authenticated user before granting any permissions. Avoid using wildcard rules like allow read, write: if true in production, as they expose your entire database to anyone.

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /users/{userId} {
      allow read, write: if request.auth != null && request.auth.uid == userId;
    }
    match /public/{document=**} {
      allow read, write: if request.auth != null;
    }
  }
}

The rules above restrict user documents so that only the document owner can modify their data. The request.auth object contains the authenticated user's claims, including their UID. By comparing it to the path variable {userId}, we ensure users cannot access other users' data. The second rule allows any authenticated user to read and write to the public collection, which is useful for shared content like posts or comments.

MVVM Integration with Jetpack Compose

Jetpack Compose is Android's modern toolkit for building native UI. It integrates naturally with ViewModels and Firebase repositories. To create a complete MVVM architecture, you should separate concerns into presentation, domain, and data layers. The ViewModel holds the business logic and exposes state to the UI via StateFlows or SharedStates.

In Compose, you collect StateFlows using collectAsStateWithLifecycle() to observe changes reactively. This function respects the lifecycle and prevents memory leaks. Your UI composables should only observe state and trigger actions through ViewModel methods. Avoid placing Firebase calls directly in composables, as this couples your UI to the data layer and makes testing difficult.

@Composable
fun UserScreen(viewModel: UserViewModel = viewModel()) {
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()
    
    when (uiState) {
        is UiState.Loading -> CircularProgressIndicator()
        is UiState.Success -> UserList(users = uiState.users)
        is UiState.Error -> Text("Error: ${(uiState as UiState.Error).message}")
    }
}

The composable above demonstrates a clean separation of concerns. It observes the ViewModel's state and displays different UI elements based on the current state. The UserList composable receives the list of users and renders them. This pattern ensures your UI is predictable and easily testable.

Common Pitfalls

Developers frequently encounter several issues when integrating Firebase. First, forgetting to add the google-services.json file to the correct module will cause build failures. Always verify the file is in the app module root. Second, using outdated Firebase library versions can lead to compatibility issues. Always use the BOM to manage versions and keep it updated.

Another common mistake is not handling authentication state changes properly. Firebase sessions persist across app restarts, so your app should check for an existing user on startup. If no user is signed in, redirect to the login screen instead of assuming the user is anonymous. Additionally, always handle network errors gracefully. Firestore and Storage operations can fail due to poor connectivity, so implement retry logic or user-friendly error messages.

Finally, avoid exposing sensitive data in client-side rules. Security rules should enforce authorization on the server side, not rely on client-side checks. Never trust data coming from the client without validation. Use Firebase App Check to protect your backend resources from unauthorized access.

Conclusion

Integrating Firebase into your Android app provides powerful backend capabilities with minimal setup. By following the steps in this tutorial, you have learned to configure Firebase, implement authentication, work with Firestore and Storage, write security rules, and structure your code using MVVM and Jetpack Compose. These skills form the foundation for building scalable, secure, and responsive Android applications. Experiment with additional Firebase services like Analytics and Crashlytics to further enhance your app's reliability and user insights.