Chapter 9: Firebase Integration

What is Firebase?

Firebase is a **BaaS (Backend-as-a-Service)** platform developed by Google. It provides a complete, cloud-hosted backend for your application, allowing you to build full-stack apps *without* writing a single line of server code yourself.

Think about a feature like "user login." To build this from scratch, you would need to:

  1. Set up a server (e.g., Node.js, Django).
  2. Create a database (e.g., MongoDB, PostgreSQL).
  3. Write API endpoints for `/signup`, `/login`.
  4. Implement secure password hashing (bcrypt).
  5. Create and manage user sessions with JWTs.
  6. Deploy and maintain this server 24/7.

With Firebase, you just add their SDK, write FirebaseAuth.getInstance().createUserWithEmail(...), and you're done. Firebase handles the server, database, and security for you.

Firebase is a *suite* of products. For this chapter, we will cover the four most essential services for Android developers:

  • Firebase Authentication: For user sign-up, login, and management (Email, Google, Phone).
  • Cloud Firestore: A real-time NoSQL database for storing your app's data (user profiles, posts, etc.).
  • Cloud Storage for Firebase: For storing large user-generated files (profile pictures, videos, PDFs).
  • Firebase Cloud Messaging (FCM): For sending push notifications to your users.

Firebase Setup: Connecting Your App

Before you can use any Firebase service, you must first create a Firebase project and connect it to your Android app.

Step 1: Create a Firebase Project

  1. Go to the Firebase Console.
  2. Log in with your Google account.
  3. Click **"Add project"**.
  4. Give your project a name (e.g., "MyTestApp").
  5. (Optional but Recommended) Enable Google Analytics for this project.
  6. Click "Create project."

Step 2: Register Your Android App

  1. Inside your new project's dashboard, click the **Android icon** (</>) to add an Android app.
  2. You will be asked for three things:
    • Android package name: This is the *most important* step. This MUST be the **exact same** as the applicationId in your build.gradle.kts file (e.g., com.codewithmsmaxpro.myapp).
    • App nickname (Optional): A simple name, like "My Android App".
    • SHA-1 certificate (Optional): You **need** this for Google Sign-In and other security features.

How to get your SHA-1 Key (Critical Step)

Open your Android Studio project. In the Gradle tab (usually on the right), navigate to:
YourApp > Tasks > android > signingReport
Double-click signingReport. A "Run" window will open, and you will see several outputs. Find the **"debug"** variant and copy its **SHA-1** key. It looks like `A1:B2:C3:...:F9`.

Paste this SHA-1 key into the Firebase setup. If you forget, you can add it later in Project Settings.

Step 3: Download and Add `google-services.json`

  1. After registering, Firebase will ask you to download a config file: google-services.json.
  2. Download this file.
  3. In Android Studio, switch your Project Panel from "Android" view to **"Project"** view.
  4. Drag and drop the google-services.json file into your **`app`** folder (at the same level as your src and build.gradle.kts files).
  5. This is very important. This file contains all your project's API keys and identifiers. **Never** share this file publicly or commit it to a public GitHub repository.

Step 4: Add Firebase SDKs (Gradle Setup)

Firebase will tell you to add its SDKs to your Gradle files.

build.gradle.kts (Project-level)
plugins {
    // ...
    // Add the Google Services plugin
    alias(libs.plugins.google.gms.google.services) apply false
}
build.gradle.kts (Module-level: :app)
plugins {
    // ...
    // Apply the Google Services plugin
    alias(libs.plugins.google.gms.google.services)
}

dependencies {
    // ...
    // Add the Firebase Bill of Materials (BOM)
    // This manages all Firebase library versions for you
    implementation(platform("com.google.firebase:firebase-bom:32.3.1"))
    
    // Now, you can add the specific Firebase products you need
    // WITHOUT specifying a version number.
    implementation("com.google.firebase:firebase-analytics-ktx")
    implementation("com.google.firebase:firebase-auth-ktx")
    implementation("com.google.firebase:firebase-firestore-ktx")
}

Click **"Sync Now"**. That's it! Your app is now connected to Firebase.

Pillar 1: Firebase Authentication

Firebase Authentication provides a complete backend service to handle user sign-up and login. It supports email/password, phone numbers, and federated providers like Google, Facebook, and Twitter, all with a single SDK.

Step 1: Enable Auth Methods in Console

Before you can use any login method, you must "turn it on."

  1. Go to the Firebase Console.
  2. Open your project.
  3. In the left menu, go to **Build > Authentication**.
  4. Click the **"Get started"** button.
  5. Go to the **"Sign-in method"** tab.
  6. Click on **"Email/Password"** and **Enable** it.
  7. Click on **"Google"**, **Enable** it, and provide a project support email.

Step 2: Add Auth Dependency

If you haven't already, add the Auth dependency to your `build.gradle.kts` file:

implementation("com.google.firebase:firebase-auth-ktx")

Step 3: Get the `FirebaseAuth` Instance

The `FirebaseAuth` object is your single entry point for all auth operations. You can get an instance of it anywhere.

import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.auth.ktx.auth
import com.google.firebase.ktx.Firebase

class MyAuthManager {
    private val auth: FirebaseAuth = Firebase.auth
}

Deep Dive: Email/Password Sign-Up

This function creates a new user in the Firebase backend.

AuthViewModel.kt
fun signUp(email: String, password: String) {
    viewModelScope.launch {
        try {
            val result = auth.createUserWithEmailAndPassword(email, password).await()
            val firebaseUser = result.user
            Log.d("Auth", "Sign up successful! User ID: ${firebaseUser?.uid}")
            // Now, you should probably save this user's info to Firestore
            
        } catch (e: FirebaseAuthUserCollisionException) {
            Log.w("Auth", "Email already in use.")
        } catch (e: FirebaseAuthWeakPasswordException) {
            Log.w("Auth", "Password is too weak.")
        } catch (e: Exception) {
            Log.e("Auth", "Sign up failed: ${e.message}")
        }
    }
}

Note: We use .await() from the kotlinx-coroutines-play-services library to make the old "Task" API work with modern coroutines.

Deep Dive: Email/Password Sign-In

This function logs in an existing user.

AuthViewModel.kt
fun signIn(email: String, password: String) {
    viewModelScope.launch {
        try {
            val result = auth.signInWithEmailAndPassword(email, password).await()
            val firebaseUser = result.user
            Log.d("Auth", "Sign in successful! Welcome, ${firebaseUser?.email}")
            // Navigate to home screen
            
        } catch (e: FirebaseAuthInvalidUserException) {
            Log.w("Auth", "No account found with this email.")
        } catch (e: FirebaseAuthInvalidCredentialsException) {
            Log.w("Auth", "Wrong password.")
        } catch (e: Exception) {
            Log.e("Auth", "Sign in failed: ${e.message}")
        }
    }
}

Observing Auth State (The "Current User")

How do you know if a user is logged in *right now*? Firebase Auth maintains the user's session for you. You can get the current user at any time.

val currentUser = auth.currentUser
if (currentUser != null) {
    // User is logged in
    println("Current user ID: ${currentUser.uid}")
} else {
    // User is logged out
    println("No user is logged in.")
}

Deep Dive: Google Sign-In

This is more complex because it involves another SDK (Google Play Services) to get the Google token, which you then "exchange" for a Firebase credential.

Step 1: Add Google Sign-In Dependency

implementation("com.google.android.gms:play-services-auth:20.7.0")

Step 2: Set up GoogleSignInClient (in your Activity)

import com.google.android.gms.auth.api.signin.GoogleSignIn
import com.google.android.gms.auth.api.signin.GoogleSignInClient
import com.google.android.gms.auth.api.signin.GoogleSignInOptions

lateinit var googleSignInClient: GoogleSignInClient

override fun onCreate(savedInstanceState: Bundle?) {
    // ...
    // 1. Configure Google Sign-In
    val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
        // You get this from your google-services.json file
        .requestIdToken(getString(R.string.default_web_client_id))
        .requestEmail()
        .build()

    // 2. Build the client
    googleSignInClient = GoogleSignIn.getClient(this, gso)
}

Step 3: Register an Activity Result Launcher

This is the modern way to handle the result from the Google Sign-In pop-up.

private val googleSignInLauncher = registerForActivityResult(
    ActivityResultContracts.StartActivityForResult()
) { result ->
    if (result.resultCode == Activity.RESULT_OK) {
        val task = GoogleSignIn.getSignedInAccountFromIntent(result.data)
        try {
            val account = task.getResult(ApiException::class.java)
            firebaseAuthWithGoogle(account.idToken)
        } catch (e: ApiException) {
            Log.w("Auth", "Google sign in failed", e)
        }
    }
}

Step 4: Start the Sign-In Flow (e.g., on button click)

googleSignInButton.setOnClickListener {
    val signInIntent = googleSignInClient.signInIntent
    googleSignInLauncher.launch(signInIntent)
}

Step 5: Exchange the Google Token for a Firebase Credential

private fun firebaseAuthWithGoogle(idToken: String?) {
    val credential = GoogleAuthProvider.getCredential(idToken, null)
    viewModelScope.launch {
        try {
            auth.signInWithCredential(credential).await()
            Log.d("Auth", "Firebase Google Sign-In Successful")
            // Navigate to home screen
        } catch (e: Exception) {
            Log.w("Auth", "Firebase Google Sign-In Failed", e)
        }
    }
}
Read More about Firebase Google Sign-In →

Pillar 2: Cloud Firestore

Firebase Authentication only confirms *who* a user is. It doesn't store any *data* about them (like their username, bio, or high score). For that, you need a database. **Cloud Firestore** is Firebase's modern, powerful, and scalable NoSQL database.

Firestore vs. Firebase Realtime Database (RTDB)

Firebase offers *two* databases, which is confusing.

  • Realtime Database (RTDB): This is the *original* Firebase database. It's one giant JSON file. It's very fast, but it struggles with complex data and querying.
  • Cloud Firestore (Newer): This is the *new and recommended* database. It's **document-based** (NoSQL), which is much more structured. It has more powerful querying, scales better, and is more robust.

Rule: Always use Cloud Firestore for new projects.

Firestore Data Model (Critical Concept)

Firestore is a NoSQL database. It doesn't have tables and rows. Instead, it has:

  • Collection: A folder or list of documents (e.g., "users", "posts").
  • Document: A single entry in a collection. It's like a container. Each document has a unique **Document ID**.
  • Fields (Data): The actual key-value data *inside* a document (e.g., name: "Aman", score: 100).

**Analogy:** A Collection ("users") is a filing cabinet. A Document ("user_123") is a single file folder. The Fields are the papers *inside* that folder (name: "Aman").

Important: You *cannot* have documents directly inside other documents. You can only have **Collections** inside **Documents** (called "sub-collections").

Step 1: Add Firestore Dependency

implementation("com.google.firebase:firebase-firestore-ktx")

Step 2: Get the Firestore Instance

import com.google.firebase.firestore.FirebaseFirestore
import com.google.firebase.firestore.ktx.firestore
import com.google.firebase.ktx.Firebase

val db: FirebaseFirestore = Firebase.firestore

Step 3: Writing Data (Create & Update)

To write data, you first create a Kotlin data class or a Map.

data class UserProfile(
    val username: String = "",
    val email: String = "",
    val joinDate: Long = System.currentTimeMillis()
)

fun saveUserToFirestore() {
    val userId = auth.currentUser?.uid ?: return
    val email = auth.currentUser?.email ?: ""

    val newUser = UserProfile(username = "MSMAXPRO", email = email)

    // 1. Get a reference to the document
    // This creates a path: /users/USER_ID
    val userDocument = db.collection("users").document(userId)

    // 2. Set the data (this will CREATE or OVERWRITE)
    userDocument.set(newUser)
        .addOnSuccessListener { Log.d("Firestore", "User profile saved!") }
        .addOnFailureListener { e -> Log.e("Firestore", "Error saving", e) }
}

fun addPost(title: String) {
    val post = mapOf("title" to title, "likes" to 0)
    
    // .add() will auto-generate a unique document ID
    db.collection("posts")
        .add(post)
        .addOnSuccessListener { documentReference ->
            Log.d("Firestore", "Post added with ID: ${documentReference.id}")
        }
}

Step 4: Reading Data (One Time)

To get data just once (e.g., loading a user's profile), you use .get().

fun fetchUserProfile() {
    val userId = auth.currentUser?.uid ?: return
    
    db.collection("users").document(userId)
        .get()
        .addOnSuccessListener { document ->
            if (document != null && document.exists()) {
                // Convert the document back into our data class
                val user = document.toObject(UserProfile::class.java)
                Log.d("Firestore", "Username: ${user?.username}")
            } else {
                Log.d("Firestore", "No such document")
            }
        }
        .addOnFailureListener { e ->
            Log.e("Firestore", "Error getting document", e)
        }
}

Step 5: Reading Data (Real-time) - The Magic

This is the best part of Firestore. You can "listen" to a document or collection. When *anything* changes (even on another user's phone), Firestore will **automatically** send the new data to your app. This is perfect for a chat app or a live feed.

You use .addSnapshotListener() instead of .get().

ChatActivity.kt
// This listener will stay active until the Activity is destroyed
fun listenForMessages(chatRoomId: String) {
    db.collection("chatRooms").document(chatRoomId)
      .collection("messages")."timestamp")
      .addSnapshotListener { snapshots, error ->
        
        if (error != null) {
            Log.w("Firestore", "Listen failed.", error)
            return@addSnapshotListener
        }

        if (snapshots != null) {
            val messages = snapshots.toObjects(ChatMessage::class.java)
            // Update your UI (e.g., your RecyclerView or LazyColumn)
            updateChatUI(messages)
        }
      }
}
Read More about Cloud Firestore →

Pillar 3: Cloud Storage for Firebase

Firestore is great for text, numbers, and booleans. But where do you store large files like profile pictures, videos, or PDFs? **You should never store large files in a database.**

Instead, you use **Cloud Storage**. This service is built specifically to store and serve large binary files.

The common workflow is:

  1. User picks an image from their phone's gallery (Uri).
  2. You upload this image file to Cloud Storage.
  3. Storage gives you back a permanent **Download URL** (a public https://... link).
  4. You take this URL (which is just a String) and save it in your **Firestore** document (e.g., in the `profilePictureUrl` field of your User).

Step 1: Add Storage Dependency

implementation("com.google.firebase:firebase-storage-ktx")

Step 2: Set Security Rules

By default, no one can read or write to your Storage bucket. You must go to the **Firebase Console > Storage > Rules** tab and set up rules. For development, you can start with this (INSECURE):

// WARNING: Insecure, allows anyone to read/write.
rules_version = '2';
service firebase.storage {
  match /b/{bucket}/o {
    match /{allPaths=**} {
      allow read, write: if true;
    }
  }
}

Step 3: Uploading a File

import com.google.firebase.storage.ktx.storage
import android.net.Uri

val storage = Firebase.storage
val storageRef = storage.reference

// Assume 'imageUri' is the Uri you got from the user's photo gallery
fun uploadProfilePic(userId: String, imageUri: Uri) {
    // 1. Create a reference. e.g., "profile_images/USER_ID_123.jpg"
    val imageRef = storageRef.child("profile_images/$userId.jpg")
    
    // 2. Start the upload task
    val uploadTask = imageRef.putFile(imageUri)

    // 3. Listen for success or failure
    uploadTask.addOnSuccessListener { taskSnapshot ->
        Log.d("Storage", "Image uploaded!")
        
        // 4. IMPORTANT: Get the Download URL
        getDownloadUrl(imageRef)
        
    }.addOnFailureListener { e ->
        Log.e("Storage", "Upload failed", e)
    }
}

fun getDownloadUrl(imageRef: StorageReference) {
    imageRef.downloadUrl.addOnSuccessListener { uri ->
        val imageUrl = uri.toString()
        Log.d("Storage", "Download URL: $imageUrl")
        
        // 5. Now save this URL string to your user's document in Firestore
        saveUrlToFirestore(imageUrl)
    }
}
Read More about Cloud Storage →

Pillar 4: Firebase Cloud Messaging (FCM)

FCM is the service that lets you send **Push Notifications** to your app, even when the app is closed. This is extremely powerful for user re-engagement (e.g., "You have a new message!" or "A new post is available").

How it Works

  1. When your app is installed, it registers with FCM and receives a unique **FCM Registration Token**.
  2. Your app sends this token to your backend (e.g., saves it to your Firestore user document).
  3. When you want to send a notification, your *server* (not your app) tells the FCM API: "Send this message to *this* token."
  4. FCM handles delivering the message to the correct device.

Step 1: Add FCM Dependency

implementation("com.google.firebase:firebase-messaging-ktx")

Step 2: Create a Messaging Service

You need to create a Service (see Chapter 4) that extends FirebaseMessagingService. This service will run in the background and "listen" for two things: new tokens and new messages.

services/MyFirebaseMessagingService.kt
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
import android.util.Log

class MyFirebaseMessagingService : FirebaseMessagingService() {

    /**
     * This is called when a new token is generated.
     * This happens on first app install, or if the user
     * reinstalls the app or clears data.
     */
    override fun onNewToken(token: String) {
        super.onNewToken(token)
        Log.d("FCM", "New Token Generated: $token")
        
        // **CRITICAL:** You MUST send this token to your server!
        // e.g., saveTokenToFirestore(token)
    }

    /**
     * This is called when a message is received *while the app is in the foreground*.
     * If the app is in the background, Android shows a
     * system notification tray icon automatically.
     */
    override fun onMessageReceived(remoteMessage: RemoteMessage) {
        super.onMessageReceived(remoteMessage)
        
        Log.d("FCM", "Message received!")
        
        remoteMessage.notification?.let { notification ->
            Log.d("FCM", "Title: ${notification.title}")
            Log.d("FCM", "Body: ${notification.body}")
            
            // If the app is in the foreground, the system won't show it.
            // So, you have to build a custom notification here.
            showCustomNotification(notification.title, notification.body)
        }
    }
    
    private fun showCustomNotification(title: String?, body: String?) {
        // ... (Code to build and show a Notification) ...
    }
}

Step 3: Register the Service in `AndroidManifest.xml`

Just like any service, you must register it.

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

That's it! Your app is now ready to receive notifications. You can send your first test notification from the **Firebase Console > Engage > Cloud Messaging** dashboard.

Read More about Firebase Cloud Messaging →