Build a User Management App
Build an Android app with Firebase Auth, Firestore, and Room Database.
Introduction
30 Second Summary
Every app you use stores your data somewhere. Your banking app keeps your information on a remote server, but it also caches it locally so things load instantly even with a bad connection.
In this project, you will build a user management Android app that stores user data in both a cloud database and a local database. You will implement sign-up, sign-in, profile editing, and real-time synchronization between the two storage layers.
What You'll Build
A three-screen Android app (Sign Up, Sign In, Profile) powered by Firebase Authentication for login, Cloud Firestore for cloud storage, and Room Database for local storage.
By the end of this project, you'll have:
- A Firebase project with Authentication and Firestore connected to your Android app.
- Sign Up and Sign In screens with input validation and email/password authentication.
- A Profile screen that reads from Room Database (local) and syncs with Cloud Firestore (cloud), verified using the Database Inspector.
- Secret Mission: Add a "Delete Account" feature that removes user data from Firebase Auth, Firestore, and Room Database simultaneously.
Are there any prerequisites?
You should have basic Kotlin knowledge (classes, functions, syntax) and the ability to create a new Android Studio project with the Empty Activity template. You also need Android Studio Panda 2 (2025.3.2) already installed on your machine.
Create a Firebase Project
Your app needs a backend to handle user accounts and store data in the cloud. Firebase provides both authentication and a cloud database out of the box, so you can focus on building the Android side.
In this step, you will create a Firebase project, connect it to your Android app, and enable the two services your app needs: Firebase Authentication and Cloud Firestore.
In this step, get ready to:
- Create a Firebase project and register your Android app.
- Download the Firebase config file and add it to your project.
- Enable Firebase Authentication and Cloud Firestore.
Create a Google account and Firebase project
Firebase is a Google service, so you need a Google account to use it. If you already have one, skip ahead to the Firebase Console step.
- If you do not have a Google account, go to accounts.google.com and click Create account. Follow the prompts to finish creating your account.
- Open your browser and go to console.firebase.google.com.
- Sign in with your Google account.
- Click the button to create a new Firebase project.
- Enter DatabasedTest as the project name.
- If prompted, review and accept the Firebase terms, then click Continue.
- Toggle off the Google Analytics option. You do not need it for this project.
- Click Create project.
- Wait for Firebase to finish provisioning, then click Continue.
You are now on the Firebase project overview page for your new project.
What is Firebase?
Firebase is a platform from Google that gives your app ready-made backend services. Instead of building your own server for user sign-in or database storage, Firebase provides these as managed services you connect to with a config file.
The Spark (free) plan includes 3,000 daily active users for Authentication and 50,000 Firestore document reads per day. No payment information is needed to get started.
Register your Android app in Firebase
Firebase needs to know which app it is connecting to. You register your app by providing its package name, then download a config file that links your local project to the Firebase backend.
- On the Firebase project overview page, click the Android icon in the center of the page to launch the setup workflow.
- In the Android package name field, enter com.example.databasedtest.
- Click Register app.
- Click Download google-services.json to download the Firebase config file.
- Open your project's app/ folder in File Explorer. The full path is C:\Users\Asus\Desktop\TTTM2213\databasedtest\app\.
- Move the downloaded google-services.json file into this app/ folder.
- Back in the Firebase Console, click Next to skip through the remaining setup steps (you will configure Gradle dependencies in the next step).
- Click Continue to console to return to the project overview.
What is google-services.json?
This file contains non-secret identifiers that connect your Android app to your Firebase project. It includes your project ID, API key, and app ID. The Google Services Gradle plugin reads this file at build time so your app knows which Firebase project to talk to.
Package name mismatch?
The package name you enter in Firebase must exactly match the applicationId in your app/build.gradle.kts file. It is case-sensitive and cannot be changed after registration. Double-check your Gradle file if you are unsure.
Make sure the downloaded file is named exactly google-services.json with no extra characters like (2) appended.
help me fix a package name mismatch between my Android project and Firebase
Enable Authentication and Cloud Firestore
Your app needs two Firebase services: Authentication for user sign-up and sign-in, and Cloud Firestore for storing user profile data in the cloud.
- In the left sidebar of the Firebase Console, click Build.
- Click Authentication.
- Click Get started.
- On the Sign-in method tab, click Email/Password.
- Toggle the Enable switch on.
- Click Save.
Email/Password authentication is now active. Next, enable the database.
- In the left sidebar, click Build again.
- Click Firestore Database.
- Click Create database.
- Select Start in test mode and click Next.
- Choose the Cloud Firestore location closest to you and click Create.
Your Firestore database is now ready. You should see an empty database dashboard with a + Start collection button.
Why test mode?
Test mode allows any authenticated user to read and write data without security rules blocking them. This is perfect for development. Before launching a real app, you would lock down the rules to restrict access.
Don't see Firestore Database in the sidebar?
Make sure you are clicking Build in the left sidebar first to expand the product list. Firestore Database appears under the Build category.
help me find and enable Cloud Firestore in the Firebase Console
Your Firebase project is fully set up with Authentication and Firestore ready to go. Next up, you will configure your Android project's Gradle files to connect to these services.
Configure Gradle Dependencies
Your Firebase project is connected, but your Android app does not know how to talk to Firebase yet. Gradle dependencies are the bridge that gives your app access to external libraries like Firebase, Room, and Kotlin Coroutines.
In this step, you will add all the plugins and libraries your project needs to the app/build.gradle.kts file. Then you will create the package structure that organizes your code into a clean MVVM architecture.
In this step, get ready to:
- Configure Gradle plugins and add all project dependencies.
- Sync the project and verify a successful build.
- Create the package structure for your app's architecture.
Update the Gradle configuration
Android projects use Gradle as their build system. You need to declare which plugins and libraries your app uses so Gradle can download and include them.
First, you will add the Google Services and KSP plugins at the project level. Then you will apply them in the app-level file along with all your dependencies.
- In the Project window (left panel), expand the root-level Gradle Scripts section.
- Open the project-level build.gradle.kts file (the one labelled Project: databasedtest).
- Add the Google Services and KSP plugins inside the existing plugins block, with apply false so they are available but not yet applied at this level:
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.android) apply false
id("com.google.gms.google-services") version "4.4.4" apply false
id("com.google.devtools.ksp") version "2.3.9" apply false
}
What does this code do?
The com.google.gms.google-services plugin connects your app to the Firebase project using the google-services.json file you downloaded earlier.
The com.google.devtools.ksp plugin (Kotlin Symbol Processing) is a code generation tool that Room Database uses to create database code from your annotations at compile time.
- Save the project-level build.gradle.kts file.
- Now open the app-level build.gradle.kts file (the one labelled Module :app).
- Replace the entire contents of this file with the following code to apply the plugins, enable View Binding, and add all dependencies:
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
id("com.google.gms.google-services")
id("com.google.devtools.ksp")
}
android {
namespace = "com.example.databasedtest"
compileSdk = 35
defaultConfig {
applicationId = "com.example.databasedtest"
minSdk = 26
targetSdk = 35
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
buildFeatures {
viewBinding = true
}
}
dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.appcompat)
implementation(libs.material)
implementation(libs.androidx.activity)
implementation(libs.androidx.constraintlayout)
// Firebase
implementation(platform("com.google.firebase:firebase-bom:34.14.1"))
implementation("com.google.firebase:firebase-auth")
implementation("com.google.firebase:firebase-firestore")
// Room Database
implementation("androidx.room:room-runtime:2.8.4")
implementation("androidx.room:room-ktx:2.8.4")
ksp("androidx.room:room-compiler:2.8.4")
// Lifecycle (ViewModel + LiveData)
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.9.0")
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.9.0")
// Coroutines
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2")
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
}
What does this code do?
The plugins block applies four plugins: the Android application plugin, Kotlin, Google Services (for Firebase), and KSP (for Room's code generation).
The buildFeatures { viewBinding = true } section enables View Binding, which generates type-safe classes to access your XML layout views without findViewById calls.
The dependencies section adds five groups of libraries:
- Firebase BOM (Bill of Materials) manages compatible versions of firebase-auth and firebase-firestore automatically.
- Room provides the local SQLite database with room-runtime (core), room-ktx (coroutine support), and room-compiler (annotation processing via KSP).
- Lifecycle libraries give you ViewModel (survives configuration changes) and LiveData (observable data holder).
- Coroutines enable asynchronous operations (network calls, database reads) without blocking the main thread.
- Save app/build.gradle.kts.
Seeing red underlines in the plugins block?
If com.google.gms.google-services or com.google.devtools.ksp shows an error, make sure you saved the project-level build.gradle.kts first. The app-level file depends on the plugin declarations in the project-level file.
Also confirm that your project-level plugins include the correct version numbers: 4.4.4 for Google Services and 2.3.9 for KSP.
Help me fix red underlines in my Gradle plugins block.
✔️ Awesome, I've got everything!
Great. Double check you have saved both Gradle files before continuing.
ⓧ I'd like to double check the full code
Here is the complete app/build.gradle.kts for comparison:
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
id("com.google.gms.google-services")
id("com.google.devtools.ksp")
}
android {
namespace = "com.example.databasedtest"
compileSdk = 35
defaultConfig {
applicationId = "com.example.databasedtest"
minSdk = 26
targetSdk = 35
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
buildFeatures {
viewBinding = true
}
}
dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.appcompat)
implementation(libs.material)
implementation(libs.androidx.activity)
implementation(libs.androidx.constraintlayout)
// Firebase
implementation(platform("com.google.firebase:firebase-bom:34.14.1"))
implementation("com.google.firebase:firebase-auth")
implementation("com.google.firebase:firebase-firestore")
// Room Database
implementation("androidx.room:room-runtime:2.8.4")
implementation("androidx.room:room-ktx:2.8.4")
ksp("androidx.room:room-compiler:2.8.4")
// Lifecycle (ViewModel + LiveData)
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.9.0")
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.9.0")
// Coroutines
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2")
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
}
Sync the project and verify the build
After editing Gradle files, Android Studio needs to re-download and resolve all the new dependencies. This process is called a Gradle sync.
- Click the Sync Now banner that appears at the top of the editor.
- Wait for the sync to complete. You can watch the progress in the Build tool window at the bottom of Android Studio.
- Confirm that the Build output shows a success message with no errors.
What is KSP doing during the sync?
KSP (Kotlin Symbol Processing) is a compiler plugin that reads annotations in your code and generates boilerplate at build time. Room Database uses KSP to turn your @Entity and @Dao annotations into full database implementation classes, so you do not have to write raw SQL setup code yourself.
Sync failed with an error?
Check that your google-services.json file is in the app/ directory (not the project root). The Google Services plugin specifically looks for this file inside the app module folder.
If you see a version conflict error, verify that your project-level build.gradle.kts uses version 4.4.4 for the Google Services plugin and 2.3.9 for KSP.
Help me fix my Gradle sync error.
Create the package structure
A clean package structure keeps your code organized as the project grows. You will create packages that follow the MVVM (Model-View-ViewModel) architecture with a Repository pattern.
- In the Project window, switch to the Android view using the dropdown at the top.
- Expand app > java > com.example.databasedtest.
- Right-click on com.example.databasedtest and select New > Package.
- Type ui and press Enter.
Repeat the same process (right-click the base package, select New > Package) to create each of the following packages:
- data.local.dao
- data.local.database
- data.local.entity
- data.remote
- data.repository
- viewmodel
- utils
What is each package for?
- ui holds your Activity classes (the screens the user sees).
- data.local.entity holds Room entity classes that define your database tables.
- data.local.dao holds Data Access Objects that define database queries.
- data.local.database holds the Room database class itself.
- data.remote is for remote data source classes (Firebase calls).
- data.repository holds the Repository that coordinates local and remote data.
- viewmodel holds ViewModel classes that manage UI state.
- utils is for helper and utility classes.
- Verify that your package structure is visible by expanding the com.example.databasedtest folder in the Project window.
- You should see all eight packages (ui, data with its subpackages, viewmodel, utils) listed under your base package.
Packages not showing up?
Make sure you are in the Android view (not Project view). Empty packages may be hidden by default. Click the gear icon at the top of the Project window and uncheck Compact Middle Packages to see all levels of the hierarchy.
Help me find my packages in Android Studio.
Your project is configured and organized. Next up, you will build the Room Database layer that stores user data locally on the device.
Build the Room Database Layer
Your Gradle dependencies are in place and your project syncs without errors. Now it is time to build the data layer that stores user information locally on the device.
Android apps that rely only on a cloud database become unusable the moment the network drops. By adding a local Room Database alongside Cloud Firestore, your app can read user data instantly from the device and sync with the cloud in the background.
In this step, get ready to:
- Create the Room Entity and DAO that define your local database schema.
- Build the AppDatabase class and UserRepository that unify local and cloud data access.
- Verify the database using the Database Inspector.
Create the User entity
Room needs a data class annotated as an Entity to know what table to create and what columns it should have. This class maps directly to a row in your local SQLite database.
- In Android Studio, open the data/local/entity package in the Project pane on the left.
- Right-click the package, select New then Kotlin Class/File.
- Name it User and select Data Class.
- Replace all the contents of the file with the following code:
package com.example.databasedtest.data.local.entity
import androidx.room.Entity
import androidx.room.PrimaryKey
// Marks this class as a Room database table named "user_table"
@Entity(tableName = "user_table")
data class User(
// The Firebase UID serves as the unique primary key
@PrimaryKey
val uid: String,
val username: String,
val email: String,
val phoneNumber: String
)
What does this code do?
The @Entity annotation tells Room to create a SQLite table called user_table. Each property in the data class becomes a column.
The @PrimaryKey annotation on uid ensures every row is uniquely identified by the Firebase user ID. This makes it easy to look up a specific user later.
- Save User.kt.
Seeing unresolved reference errors?
Make sure the package declaration at the top matches com.example.databasedtest.data.local.entity exactly. If the imports for androidx.room are red, re-sync Gradle by clicking the elephant icon in the toolbar.
Still stuck? Help me fix unresolved Room imports in my User entity
Create the UserDao interface
A DAO (Data Access Object) defines the operations your app can perform on the database table. Room generates the SQL implementation for you at compile time.
- Right-click the data/local/dao package.
- Select New then Kotlin Class/File.
- Name it UserDao and select Interface.
- Replace all the contents with the following code:
package com.example.databasedtest.data.local.dao
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Update
import com.example.databasedtest.data.local.entity.User
@Dao
interface UserDao {
// Insert a user, replacing any existing record with the same primary key
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertUser(user: User)
// Update an existing user record
@Update
suspend fun updateUser(user: User)
// Retrieve a single user by their Firebase UID
@Query("SELECT * FROM user_table WHERE uid = :uid")
suspend fun getUserByUid(uid: String): User?
// Delete a user record by their Firebase UID
@Query("DELETE FROM user_table WHERE uid = :uid")
suspend fun deleteUser(uid: String)
}
What does this code do?
Each function is marked suspend so it runs off the main thread using Kotlin Coroutines. This prevents your app from freezing while waiting for the database.
The @Insert annotation with OnConflictStrategy.REPLACE means if a user with the same UID already exists, Room overwrites the old record. The @Query annotations let you write raw SQL for custom lookups and deletions.
- Save UserDao.kt.
Red underline on the User import?
Confirm the import path reads com.example.databasedtest.data.local.entity.User. If Android Studio cannot resolve it, make sure you saved User.kt in the previous step and that its package line matches.
Need more help? Help me resolve the User import in my UserDao
Create the AppDatabase class
The database class ties your Entity and DAO together into a single Room database instance. You will use a singleton pattern so only one database connection exists at a time.
- Right-click the data/local/database package.
- Select New then Kotlin Class/File.
- Name it AppDatabase and select Class.
- Replace all the contents with the following code:
package com.example.databasedtest.data.local.database
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import com.example.databasedtest.data.local.dao.UserDao
import com.example.databasedtest.data.local.entity.User
// Declares this as a Room database with the User entity, version 1
@Database(entities = [User::class], version = 1, exportSchema = false)
abstract class AppDatabase : RoomDatabase() {
// Exposes the UserDao so other classes can access database operations
abstract fun userDao(): UserDao
companion object {
// Volatile ensures all threads see the latest INSTANCE value
@Volatile
private var INSTANCE: AppDatabase? = null
// Singleton: returns the existing database or creates a new one
fun getDatabase(context: Context): AppDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"app_database"
).build()
INSTANCE = instance
instance
}
}
}
}
What is the singleton pattern doing here?
Opening multiple database connections wastes memory and can cause data corruption. The synchronized block ensures that even if two threads call getDatabase() at the same time, only one instance is ever created.
The @Volatile annotation guarantees every thread reads the most up-to-date value of INSTANCE from main memory instead of a stale cached copy.
- Save AppDatabase.kt.
Create the UserRepository
The Repository Pattern gives the rest of your app a single class to call for all data operations. It hides whether data comes from Room (local) or Firestore (cloud).
- Right-click the data/repository package.
- Select New then Kotlin Class/File.
- Name it UserRepository and select Class.
- Replace all the contents with the following code:
package com.example.databasedtest.data.repository
import com.example.databasedtest.data.local.dao.UserDao
import com.example.databasedtest.data.local.entity.User
import com.google.firebase.Firebase
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.firestore.firestore
import kotlinx.coroutines.tasks.await
class UserRepository(private val userDao: UserDao) {
// Firebase Firestore instance for cloud storage
private val firestore = Firebase.firestore
// Firebase Auth instance for authentication operations
private val auth = FirebaseAuth.getInstance()
// Creates a new user account, saves profile to Firestore AND Room
suspend fun signUp(email: String, password: String, username: String, phoneNumber: String): Result<User> {
return try {
val authResult = auth.createUserWithEmailAndPassword(email, password).await()
val uid = authResult.user?.uid ?: throw Exception("User ID is null")
val user = User(uid = uid, username = username, email = email, phoneNumber = phoneNumber)
// Save user profile to Firestore (cloud)
val userData = hashMapOf(
"uid" to uid,
"username" to username,
"email" to email,
"phoneNumber" to phoneNumber
)
firestore.collection("users").document(uid).set(userData).await()
// Save user profile to Room (local)
userDao.insertUser(user)
Result.success(user)
} catch (e: Exception) {
Result.failure(e)
}
}
// Signs in an existing user, fetches profile from Firestore, caches in Room
suspend fun signIn(email: String, password: String): Result<User> {
return try {
val authResult = auth.signInWithEmailAndPassword(email, password).await()
val uid = authResult.user?.uid ?: throw Exception("User ID is null")
// Fetch user profile from Firestore
val document = firestore.collection("users").document(uid).get().await()
val user = User(
uid = uid,
username = document.getString("username") ?: "",
email = document.getString("email") ?: "",
phoneNumber = document.getString("phoneNumber") ?: ""
)
// Cache in Room for fast local access
userDao.insertUser(user)
Result.success(user)
} catch (e: Exception) {
Result.failure(e)
}
}
// Checks if a username is already taken in Firestore
suspend fun isUsernameAvailable(username: String): Boolean {
val query = firestore.collection("users")
.whereEqualTo("username", username)
.get()
.await()
return query.isEmpty
}
// Reads user data from the local Room database
suspend fun getUserFromRoom(uid: String): User? {
return userDao.getUserByUid(uid)
}
// Updates user profile in both Firestore and Room
suspend fun updateUserProfile(user: User): Result<Unit> {
return try {
val userData = hashMapOf(
"uid" to user.uid,
"username" to user.username,
"email" to user.email,
"phoneNumber" to user.phoneNumber
)
firestore.collection("users").document(user.uid).set(userData).await()
userDao.updateUser(user)
Result.success(Unit)
} catch (e: Exception) {
Result.failure(e)
}
}
// Signs out the current user from Firebase Auth
fun signOut() {
auth.signOut()
}
// Returns the UID of the currently signed-in user, or null
fun getCurrentUserId(): String? {
return auth.currentUser?.uid
}
}
Why write to both Firestore and Room?
Firestore keeps data safe in the cloud and accessible from any device. Room gives your app instant reads without waiting for a network response.
The repository writes to both on sign-up and sign-in so the local cache is always fresh. Later, the Profile screen reads from Room first for speed and can refresh from Firestore in the background.
- Save UserRepository.kt.
Cannot resolve Firebase or await imports?
Confirm your app/build.gradle.kts includes the Firebase BoM and the coroutines dependency. If imports are still red after saving, click File then Sync Project with Gradle Files.
Still seeing errors? Help me fix Firebase and coroutine import errors in UserRepository
✔️ Awesome, I've got everything!
Great. Make sure all four files are saved before moving on.
ⓧ I'd like to double check the full code
Here is the full code for each file at this point.
package com.example.databasedtest.data.local.entity
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "user_table")
data class User(
@PrimaryKey
val uid: String,
val username: String,
val email: String,
val phoneNumber: String
)
package com.example.databasedtest.data.local.dao
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Update
import com.example.databasedtest.data.local.entity.User
@Dao
interface UserDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertUser(user: User)
@Update
suspend fun updateUser(user: User)
@Query("SELECT * FROM user_table WHERE uid = :uid")
suspend fun getUserByUid(uid: String): User?
@Query("DELETE FROM user_table WHERE uid = :uid")
suspend fun deleteUser(uid: String)
}
package com.example.databasedtest.data.local.database
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import com.example.databasedtest.data.local.dao.UserDao
import com.example.databasedtest.data.local.entity.User
@Database(entities = [User::class], version = 1, exportSchema = false)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
companion object {
@Volatile
private var INSTANCE: AppDatabase? = null
fun getDatabase(context: Context): AppDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"app_database"
).build()
INSTANCE = instance
instance
}
}
}
}
package com.example.databasedtest.data.repository
import com.example.databasedtest.data.local.dao.UserDao
import com.example.databasedtest.data.local.entity.User
import com.google.firebase.Firebase
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.firestore.firestore
import kotlinx.coroutines.tasks.await
class UserRepository(private val userDao: UserDao) {
private val firestore = Firebase.firestore
private val auth = FirebaseAuth.getInstance()
suspend fun signUp(email: String, password: String, username: String, phoneNumber: String): Result<User> {
return try {
val authResult = auth.createUserWithEmailAndPassword(email, password).await()
val uid = authResult.user?.uid ?: throw Exception("User ID is null")
val user = User(uid = uid, username = username, email = email, phoneNumber = phoneNumber)
val userData = hashMapOf(
"uid" to uid,
"username" to username,
"email" to email,
"phoneNumber" to phoneNumber
)
firestore.collection("users").document(uid).set(userData).await()
userDao.insertUser(user)
Result.success(user)
} catch (e: Exception) {
Result.failure(e)
}
}
suspend fun signIn(email: String, password: String): Result<User> {
return try {
val authResult = auth.signInWithEmailAndPassword(email, password).await()
val uid = authResult.user?.uid ?: throw Exception("User ID is null")
val document = firestore.collection("users").document(uid).get().await()
val user = User(
uid = uid,
username = document.getString("username") ?: "",
email = document.getString("email") ?: "",
phoneNumber = document.getString("phoneNumber") ?: ""
)
userDao.insertUser(user)
Result.success(user)
} catch (e: Exception) {
Result.failure(e)
}
}
suspend fun isUsernameAvailable(username: String): Boolean {
val query = firestore.collection("users")
.whereEqualTo("username", username)
.get()
.await()
return query.isEmpty
}
suspend fun getUserFromRoom(uid: String): User? {
return userDao.getUserByUid(uid)
}
suspend fun updateUserProfile(user: User): Result<Unit> {
return try {
val userData = hashMapOf(
"uid" to user.uid,
"username" to user.username,
"email" to user.email,
"phoneNumber" to user.phoneNumber
)
firestore.collection("users").document(user.uid).set(userData).await()
userDao.updateUser(user)
Result.success(Unit)
} catch (e: Exception) {
Result.failure(e)
}
}
fun signOut() {
auth.signOut()
}
fun getCurrentUserId(): String? {
return auth.currentUser?.uid
}
}
Verify with Database Inspector
The Database Inspector lets you view your Room tables while the app is running on the emulator. This confirms your database schema was created correctly.
- Run the app on your emulator by clicking the green Run button (or press Shift+F10).
- Wait for the app to launch on the emulator. It may show a blank screen or a crash since there is no Activity set up yet. That is expected.
- In Android Studio, click View in the top menu bar.
- Select Tool Windows, then App Inspection.
- In the App Inspection panel at the bottom, click the Database Inspector tab.
- Select your running app process from the dropdown.
You should see app_database listed. Expand it and you will see user_table with its columns: uid, username, email, phoneNumber.
Why is the table empty?
The table has no rows because you have not signed up or inserted any user yet. The important thing right now is that Room created the table with the correct schema. You will see real data appear here after building the Sign Up screen in the next step.
Database not showing up in App Inspection?
Make sure the app is actually running on the emulator and the emulator uses API level 26 or higher. If the Database Inspector tab is missing, check that you selected App Inspection (not just Logcat).
If no process appears in the dropdown, stop and re-run the app. The database is only created when getDatabase() is first called at runtime. If you see nothing, the app may need to trigger that call (it will happen once you wire up the ViewModel in the next step).
Need help? Help me open the Database Inspector in Android Studio
Your local database layer is ready. Next up, you will build the authentication screens that let users sign up and sign in, and watch real data flow into this table.
Implement Firebase Authentication
Your database layer is ready. The Room entity, DAO, database, and repository classes are all in place and verified through the Database Inspector.
Now you need to give users a way to create accounts and sign in. In this step, you will build the authentication flow using Firebase Authentication with email and password. You will create a ViewModel to manage auth state, plus two screens (Sign Up and Sign In) that validate user input and talk to Firebase through your repository.
In this step, get ready to:
- Create an AuthViewModel that coordinates sign-up and sign-in logic.
- Build a Sign Up screen with input validation and username uniqueness checks.
- Build a Sign In screen with session persistence and navigation.
Create the AuthViewModel
The ViewModel sits between your UI (Activities) and the Repository. It launches coroutines to call the repository's suspend functions and exposes results via LiveData so the UI can react to authentication outcomes.
- In the viewmodel package, create a new Kotlin file called AuthViewModel.kt.
- Paste the following code into the file:
package com.example.databasedtest.viewmodel
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import com.example.databasedtest.data.local.database.AppDatabase
import com.example.databasedtest.data.local.entity.User
import com.example.databasedtest.data.repository.UserRepository
import kotlinx.coroutines.launch
class AuthViewModel(application: Application) : AndroidViewModel(application) {
private val repository: UserRepository
private val _authResult = MutableLiveData<Result<User>>()
val authResult: LiveData<Result<User>> = _authResult
private val _usernameAvailable = MutableLiveData<Boolean>()
val usernameAvailable: LiveData<Boolean> = _usernameAvailable
init {
val userDao = AppDatabase.getDatabase(application).userDao()
repository = UserRepository(userDao)
}
fun signUp(email: String, password: String, username: String, phoneNumber: String) {
viewModelScope.launch {
val available = repository.isUsernameAvailable(username)
if (!available) {
_authResult.value = Result.failure(Exception("Username already taken"))
return@launch
}
val result = repository.signUp(email, password, username, phoneNumber)
_authResult.value = result
}
}
fun signIn(email: String, password: String) {
viewModelScope.launch {
val result = repository.signIn(email, password)
_authResult.value = result
}
}
fun checkUsernameAvailability(username: String) {
viewModelScope.launch {
_usernameAvailable.value = repository.isUsernameAvailable(username)
}
}
}
What does this code do?
The AuthViewModel extends AndroidViewModel so it can access the application context to create the database instance.
- _authResult is a private MutableLiveData that the ViewModel writes to. The public authResult exposes it as read-only LiveData for Activities to observe.
- signUp first checks Firestore for username uniqueness. If the username is taken, it immediately posts a failure result without calling Firebase Auth.
- viewModelScope.launch starts a coroutine tied to the ViewModel lifecycle. If the user navigates away, the coroutine is automatically cancelled.
- Save AuthViewModel.kt.
You will not see a visible change yet. The ViewModel is a backend piece that the Sign Up and Sign In screens will use next.
✔️ Awesome, I've got everything!
Great. Make sure the file is saved before moving on.
ⓧ I'd like to double check the full code
package com.example.databasedtest.viewmodel
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import com.example.databasedtest.data.local.database.AppDatabase
import com.example.databasedtest.data.local.entity.User
import com.example.databasedtest.data.repository.UserRepository
import kotlinx.coroutines.launch
class AuthViewModel(application: Application) : AndroidViewModel(application) {
private val repository: UserRepository
private val _authResult = MutableLiveData<Result<User>>()
val authResult: LiveData<Result<User>> = _authResult
private val _usernameAvailable = MutableLiveData<Boolean>()
val usernameAvailable: LiveData<Boolean> = _usernameAvailable
init {
val userDao = AppDatabase.getDatabase(application).userDao()
repository = UserRepository(userDao)
}
fun signUp(email: String, password: String, username: String, phoneNumber: String) {
viewModelScope.launch {
val available = repository.isUsernameAvailable(username)
if (!available) {
_authResult.value = Result.failure(Exception("Username already taken"))
return@launch
}
val result = repository.signUp(email, password, username, phoneNumber)
_authResult.value = result
}
}
fun signIn(email: String, password: String) {
viewModelScope.launch {
val result = repository.signIn(email, password)
_authResult.value = result
}
}
fun checkUsernameAvailability(username: String) {
viewModelScope.launch {
_usernameAvailable.value = repository.isUsernameAvailable(username)
}
}
}
Build the Sign Up screen
The Sign Up screen collects a username, email, phone number, and password. It validates all inputs locally before sending anything to Firebase.
- In the res/layout folder, create a new layout resource file called activity_sign_up.xml.
- Replace any default content with the following XML:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="24dp"
android:gravity="center_vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Create Account"
android:textSize="28sp"
android:textStyle="bold"
android:layout_marginBottom="32dp" />
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etUsername"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Username"
android:inputType="text" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etEmail"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Email"
android:inputType="textEmailAddress" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etPhone"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Phone Number"
android:inputType="phone" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="24dp">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etPassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Password"
android:inputType="textPassword" />
</com.google.android.material.textfield.TextInputLayout>
<Button
android:id="@+id/btnSignUp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Sign Up"
android:layout_marginBottom="16dp" />
<TextView
android:id="@+id/tvSignIn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Already have an account? Sign In"
android:textColor="@color/design_default_color_primary"
android:layout_gravity="center" />
</LinearLayout>
What does this layout contain?
- Four TextInputEditText fields wrapped in TextInputLayout from Material Design. These provide floating hint labels and error styling automatically.
- A btnSignUp button that triggers registration.
- A tvSignIn text link so existing users can navigate to the Sign In screen.
- Save activity_sign_up.xml.
- Now create the Activity that uses this layout. In the ui package, create a new Kotlin file called SignUpActivity.kt.
- Paste the following code:
package com.example.databasedtest.ui
import android.content.Intent
import android.os.Bundle
import android.util.Patterns
import android.widget.Toast
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import com.example.databasedtest.databinding.ActivitySignUpBinding
import com.example.databasedtest.viewmodel.AuthViewModel
class SignUpActivity : AppCompatActivity() {
private lateinit var binding: ActivitySignUpBinding
private val viewModel: AuthViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivitySignUpBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.btnSignUp.setOnClickListener {
val username = binding.etUsername.text.toString().trim()
val email = binding.etEmail.text.toString().trim()
val phone = binding.etPhone.text.toString().trim()
val password = binding.etPassword.text.toString().trim()
if (validateInputs(username, email, phone, password)) {
binding.btnSignUp.isEnabled = false
viewModel.signUp(email, password, username, phone)
}
}
binding.tvSignIn.setOnClickListener {
startActivity(Intent(this, SignInActivity::class.java))
finish()
}
viewModel.authResult.observe(this) { result ->
binding.btnSignUp.isEnabled = true
result.onSuccess {
Toast.makeText(this, "Account created successfully!", Toast.LENGTH_SHORT).show()
startActivity(Intent(this, ProfileActivity::class.java))
finish()
}
result.onFailure { exception ->
Toast.makeText(this, "Error: ${exception.message}", Toast.LENGTH_LONG).show()
}
}
}
private fun validateInputs(username: String, email: String, phone: String, password: String): Boolean {
if (username.isEmpty() || email.isEmpty() || phone.isEmpty() || password.isEmpty()) {
Toast.makeText(this, "All fields are required", Toast.LENGTH_SHORT).show()
return false
}
if (password.length < 8) {
Toast.makeText(this, "Password must be at least 8 characters", Toast.LENGTH_SHORT).show()
return false
}
if (!Patterns.EMAIL_ADDRESS.matcher(email).matches()) {
Toast.makeText(this, "Invalid email format", Toast.LENGTH_SHORT).show()
return false
}
if (!Patterns.PHONE.matcher(phone).matches()) {
Toast.makeText(this, "Invalid phone number", Toast.LENGTH_SHORT).show()
return false
}
return true
}
}
What does this Activity do?
View Binding generates the ActivitySignUpBinding class from your XML layout. You call inflate(layoutInflater) to create the binding, then setContentView(binding.root) to display it.
- validateInputs checks that all fields are filled, the password is at least 8 characters, the email matches Patterns.EMAIL_ADDRESS, and the phone number matches Patterns.PHONE.
- The button is disabled during sign-up to prevent double-taps. It re-enables when the observer fires.
- On success, the Activity navigates to ProfileActivity and calls finish() so the user cannot press Back to return to Sign Up.
- Save SignUpActivity.kt.
Seeing unresolved references?
- If ActivitySignUpBinding is red, make sure you saved the XML layout file first. Android Studio generates the binding class from the XML.
- If ProfileActivity is red, that is expected for now. You will create it in the next step. The project will compile once all Activities exist.
- If SignInActivity is red, you will create it in the next substep below.
Why can't I resolve these references?
✔️ Awesome, I've got everything!
Great. Double check both files are saved before continuing.
ⓧ I'd like to double check the full code
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="24dp"
android:gravity="center_vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Create Account"
android:textSize="28sp"
android:textStyle="bold"
android:layout_marginBottom="32dp" />
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etUsername"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Username"
android:inputType="text" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etEmail"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Email"
android:inputType="textEmailAddress" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etPhone"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Phone Number"
android:inputType="phone" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="24dp">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etPassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Password"
android:inputType="textPassword" />
</com.google.android.material.textfield.TextInputLayout>
<Button
android:id="@+id/btnSignUp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Sign Up"
android:layout_marginBottom="16dp" />
<TextView
android:id="@+id/tvSignIn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Already have an account? Sign In"
android:textColor="@color/design_default_color_primary"
android:layout_gravity="center" />
</LinearLayout>
package com.example.databasedtest.ui
import android.content.Intent
import android.os.Bundle
import android.util.Patterns
import android.widget.Toast
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import com.example.databasedtest.databinding.ActivitySignUpBinding
import com.example.databasedtest.viewmodel.AuthViewModel
class SignUpActivity : AppCompatActivity() {
private lateinit var binding: ActivitySignUpBinding
private val viewModel: AuthViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivitySignUpBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.btnSignUp.setOnClickListener {
val username = binding.etUsername.text.toString().trim()
val email = binding.etEmail.text.toString().trim()
val phone = binding.etPhone.text.toString().trim()
val password = binding.etPassword.text.toString().trim()
if (validateInputs(username, email, phone, password)) {
binding.btnSignUp.isEnabled = false
viewModel.signUp(email, password, username, phone)
}
}
binding.tvSignIn.setOnClickListener {
startActivity(Intent(this, SignInActivity::class.java))
finish()
}
viewModel.authResult.observe(this) { result ->
binding.btnSignUp.isEnabled = true
result.onSuccess {
Toast.makeText(this, "Account created successfully!", Toast.LENGTH_SHORT).show()
startActivity(Intent(this, ProfileActivity::class.java))
finish()
}
result.onFailure { exception ->
Toast.makeText(this, "Error: ${exception.message}", Toast.LENGTH_LONG).show()
}
}
}
private fun validateInputs(username: String, email: String, phone: String, password: String): Boolean {
if (username.isEmpty() || email.isEmpty() || phone.isEmpty() || password.isEmpty()) {
Toast.makeText(this, "All fields are required", Toast.LENGTH_SHORT).show()
return false
}
if (password.length < 8) {
Toast.makeText(this, "Password must be at least 8 characters", Toast.LENGTH_SHORT).show()
return false
}
if (!Patterns.EMAIL_ADDRESS.matcher(email).matches()) {
Toast.makeText(this, "Invalid email format", Toast.LENGTH_SHORT).show()
return false
}
if (!Patterns.PHONE.matcher(phone).matches()) {
Toast.makeText(this, "Invalid phone number", Toast.LENGTH_SHORT).show()
return false
}
return true
}
}
Build the Sign In screen and update the manifest
The Sign In screen is simpler than Sign Up. It only needs email and password fields. It also checks if the user is already signed in and skips straight to the Profile screen if so.
- In the res/layout folder, create a new layout resource file called activity_sign_in.xml.
- Replace any default content with the following XML:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="24dp"
android:gravity="center_vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Welcome Back"
android:textSize="28sp"
android:textStyle="bold"
android:layout_marginBottom="32dp" />
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etEmail"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Email"
android:inputType="textEmailAddress" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="24dp">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etPassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Password"
android:inputType="textPassword" />
</com.google.android.material.textfield.TextInputLayout>
<Button
android:id="@+id/btnSignIn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Sign In"
android:layout_marginBottom="16dp" />
<TextView
android:id="@+id/tvSignUp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Don't have an account? Sign Up"
android:textColor="@color/design_default_color_primary"
android:layout_gravity="center" />
</LinearLayout>
How is this different from the Sign Up layout?
This layout only has two input fields (email and password) instead of four. The title says "Welcome Back" to signal that this is for returning users. The navigation link at the bottom points to Sign Up instead of Sign In.
- Save activity_sign_in.xml.
- In the ui package, create a new Kotlin file called SignInActivity.kt.
- Paste the following code:
package com.example.databasedtest.ui
import android.content.Intent
import android.os.Bundle
import android.widget.Toast
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import com.example.databasedtest.databinding.ActivitySignInBinding
import com.example.databasedtest.viewmodel.AuthViewModel
import com.google.firebase.auth.FirebaseAuth
class SignInActivity : AppCompatActivity() {
private lateinit var binding: ActivitySignInBinding
private val viewModel: AuthViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivitySignInBinding.inflate(layoutInflater)
setContentView(binding.root)
if (FirebaseAuth.getInstance().currentUser != null) {
startActivity(Intent(this, ProfileActivity::class.java))
finish()
return
}
binding.btnSignIn.setOnClickListener {
val email = binding.etEmail.text.toString().trim()
val password = binding.etPassword.text.toString().trim()
if (email.isEmpty() || password.isEmpty()) {
Toast.makeText(this, "All fields are required", Toast.LENGTH_SHORT).show()
return@setOnClickListener
}
binding.btnSignIn.isEnabled = false
viewModel.signIn(email, password)
}
binding.tvSignUp.setOnClickListener {
startActivity(Intent(this, SignUpActivity::class.java))
finish()
}
viewModel.authResult.observe(this) { result ->
binding.btnSignIn.isEnabled = true
result.onSuccess {
startActivity(Intent(this, ProfileActivity::class.java))
finish()
}
result.onFailure { exception ->
Toast.makeText(this, "Error: ${exception.message}", Toast.LENGTH_LONG).show()
}
}
}
}
What does this Activity do?
- FirebaseAuth.getInstance().currentUser != null checks if the user already has an active session. If yes, it jumps straight to Profile. This means returning users skip the sign-in form entirely.
- The return@setOnClickListener exits the click listener early if fields are empty, without calling Firebase.
- On success, the Activity navigates to ProfileActivity and finishes so the back stack is clean.
- Save SignInActivity.kt.
- Now update the AndroidManifest.xml file. Open it from the app/src/main directory.
- Replace its contents with the following:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Databasedtest"
tools:targetApi="31">
<activity
android:name=".ui.ProfileActivity"
android:exported="false" />
<activity
android:name=".ui.SignUpActivity"
android:exported="false" />
<activity
android:name=".ui.SignInActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
What changed in the manifest?
- android.permission.INTERNET is added so the app can reach Firebase servers.
- SignInActivity is declared as the launcher activity with the MAIN action and LAUNCHER category. This means the app opens to the Sign In screen.
- SignUpActivity and ProfileActivity are declared but not exported, since they are launched internally by other Activities.
- Save AndroidManifest.xml.
- Run the app on your emulator. You should see the "Welcome Back" Sign In screen appear.
- Tap the "Don't have an account? Sign Up" link. The Sign Up screen should appear with four input fields.
App crashing on launch?
- Check that the android:name values in the manifest match your actual class paths. They should start with .ui. (a dot followed by the package path relative to your app namespace).
- If you see a "class not found" error, make sure ProfileActivity exists. You can create an empty placeholder file at ui/ProfileActivity.kt with just the class declaration for now. You will fill it in during the next step.
- Verify that the INTERNET permission is placed outside the <application> tag but inside <manifest>.
Help me fix my app crash on launch
✔️ Awesome, I've got everything!
Great. Make sure all files are saved and the app launches to the Sign In screen on your emulator.
ⓧ I'd like to double check the full code
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="24dp"
android:gravity="center_vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Welcome Back"
android:textSize="28sp"
android:textStyle="bold"
android:layout_marginBottom="32dp" />
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etEmail"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Email"
android:inputType="textEmailAddress" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="24dp">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etPassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Password"
android:inputType="textPassword" />
</com.google.android.material.textfield.TextInputLayout>
<Button
android:id="@+id/btnSignIn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Sign In"
android:layout_marginBottom="16dp" />
<TextView
android:id="@+id/tvSignUp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Don't have an account? Sign Up"
android:textColor="@color/design_default_color_primary"
android:layout_gravity="center" />
</LinearLayout>
package com.example.databasedtest.ui
import android.content.Intent
import android.os.Bundle
import android.widget.Toast
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import com.example.databasedtest.databinding.ActivitySignInBinding
import com.example.databasedtest.viewmodel.AuthViewModel
import com.google.firebase.auth.FirebaseAuth
class SignInActivity : AppCompatActivity() {
private lateinit var binding: ActivitySignInBinding
private val viewModel: AuthViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivitySignInBinding.inflate(layoutInflater)
setContentView(binding.root)
if (FirebaseAuth.getInstance().currentUser != null) {
startActivity(Intent(this, ProfileActivity::class.java))
finish()
return
}
binding.btnSignIn.setOnClickListener {
val email = binding.etEmail.text.toString().trim()
val password = binding.etPassword.text.toString().trim()
if (email.isEmpty() || password.isEmpty()) {
Toast.makeText(this, "All fields are required", Toast.LENGTH_SHORT).show()
return@setOnClickListener
}
binding.btnSignIn.isEnabled = false
viewModel.signIn(email, password)
}
binding.tvSignUp.setOnClickListener {
startActivity(Intent(this, SignUpActivity::class.java))
finish()
}
viewModel.authResult.observe(this) { result ->
binding.btnSignIn.isEnabled = true
result.onSuccess {
startActivity(Intent(this, ProfileActivity::class.java))
finish()
}
result.onFailure { exception ->
Toast.makeText(this, "Error: ${exception.message}", Toast.LENGTH_LONG).show()
}
}
}
}
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Databasedtest"
tools:targetApi="31">
<activity
android:name=".ui.ProfileActivity"
android:exported="false" />
<activity
android:name=".ui.SignUpActivity"
android:exported="false" />
<activity
android:name=".ui.SignInActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
Your authentication screens are wired up and ready. Next, you will build the Profile screen that displays user data from Room and syncs it with Firestore.
Build the Profile Screen with Firestore and Room Sync
Your app can now sign users up and sign them in. Both flows store user data in Cloud Firestore and Room Database simultaneously.
The missing piece is a Profile screen where authenticated users can view and edit their information. This screen reads from Room for instant local display, writes changes back to both databases, and includes a logout button that ends the session.
In this step, get ready to:
- Create the Profile screen layout with editable fields for username, email, and phone number.
- Build a ProfileViewModel that loads user data from Room and saves updates to both Firestore and Room.
- Implement logout with session clearing and verify data persistence using the Database Inspector.
Create the Profile screen layout
The Profile screen needs editable fields so users can update their information, plus buttons to save changes and log out.
- In the res/layout/ directory, create a new file called activity_profile.xml and paste the following layout:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="24dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="My Profile"
android:textSize="28sp"
android:textStyle="bold"
android:layout_marginBottom="32dp" />
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etUsername"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Username"
android:inputType="text" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etEmail"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Email"
android:inputType="textEmailAddress" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="24dp">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etPhone"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Phone Number"
android:inputType="phone" />
</com.google.android.material.textfield.TextInputLayout>
<Button
android:id="@+id/btnSave"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Save Changes"
android:layout_marginBottom="16dp" />
<Button
android:id="@+id/btnLogout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Logout"
style="@style/Widget.MaterialComponents.Button.OutlinedButton" />
</LinearLayout>
What does this layout do?
This layout uses TextInputLayout with TextInputEditText fields for username, email, and phone number. These fields are editable so the user can update their profile.
The btnSave button triggers an update to both databases. The btnLogout button uses an outlined style to visually distinguish it from the primary save action.
- Save activity_profile.xml.
Create the ProfileViewModel
The ProfileViewModel manages loading user data from Room and coordinating updates to both databases through the repository.
- In the viewmodel/ package, create a new file called ProfileViewModel.kt and paste the following code:
package com.example.databasedtest.viewmodel
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import com.example.databasedtest.data.local.database.AppDatabase
import com.example.databasedtest.data.local.entity.User
import com.example.databasedtest.data.repository.UserRepository
import kotlinx.coroutines.launch
class ProfileViewModel(application: Application) : AndroidViewModel(application) {
private val repository: UserRepository
private val _user = MutableLiveData<User?>()
val user: LiveData<User?> = _user
private val _updateResult = MutableLiveData<Result<Unit>>()
val updateResult: LiveData<Result<Unit>> = _updateResult
init {
val userDao = AppDatabase.getDatabase(application).userDao()
repository = UserRepository(userDao)
}
// Load user data from Room (fast local read)
fun loadUser() {
viewModelScope.launch {
val uid = repository.getCurrentUserId()
if (uid != null) {
val user = repository.getUserFromRoom(uid)
_user.value = user
}
}
}
// Update profile in both Firestore and Room
fun updateProfile(username: String, email: String, phoneNumber: String) {
viewModelScope.launch {
val uid = repository.getCurrentUserId() ?: return@launch
val updatedUser = User(uid = uid, username = username, email = email, phoneNumber = phoneNumber)
val result = repository.updateUserProfile(updatedUser)
_updateResult.value = result
if (result.isSuccess) {
_user.value = updatedUser
}
}
}
// Sign out the current user
fun signOut() {
repository.signOut()
}
}
What does this ViewModel do?
The loadUser() function reads from Room using the current Firebase user's UID. This gives an instant local read because the data was already cached during sign-up or sign-in.
The updateProfile() function creates an updated User object and passes it to repository.updateUserProfile(). This writes to Firestore first, then updates Room. Both databases stay in sync.
- Save ProfileViewModel.kt.
Seeing unresolved references?
If UserRepository or AppDatabase show as unresolved, make sure your import statements match the package names exactly. The repository lives at com.example.databasedtest.data.repository.UserRepository.
Help me check my import statements and package structure
Create the ProfileActivity and test logout
The Activity connects the layout to the ViewModel. It loads user data on creation, handles save button taps, and implements logout by clearing the Firebase session and navigation back stack.
- In the ui/ package, create a new file called ProfileActivity.kt and paste the following code:
package com.example.databasedtest.ui
import android.content.Intent
import android.os.Bundle
import android.widget.Toast
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import com.example.databasedtest.databinding.ActivityProfileBinding
import com.example.databasedtest.viewmodel.ProfileViewModel
class ProfileActivity : AppCompatActivity() {
private lateinit var binding: ActivityProfileBinding
private val viewModel: ProfileViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityProfileBinding.inflate(layoutInflater)
setContentView(binding.root)
// Load user data from Room on screen open
viewModel.loadUser()
// Observe user data and populate the fields
viewModel.user.observe(this) { user ->
user?.let {
binding.etUsername.setText(it.username)
binding.etEmail.setText(it.email)
binding.etPhone.setText(it.phoneNumber)
}
}
// Save button: validate and update both databases
binding.btnSave.setOnClickListener {
val username = binding.etUsername.text.toString().trim()
val email = binding.etEmail.text.toString().trim()
val phone = binding.etPhone.text.toString().trim()
if (username.isEmpty() || email.isEmpty() || phone.isEmpty()) {
Toast.makeText(this, "All fields are required", Toast.LENGTH_SHORT).show()
return@setOnClickListener
}
viewModel.updateProfile(username, email, phone)
}
// Logout button: clear session and navigate to Sign In
binding.btnLogout.setOnClickListener {
viewModel.signOut()
val intent = Intent(this, SignInActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
startActivity(intent)
finish()
}
// Observe update results for success/error feedback
viewModel.updateResult.observe(this) { result ->
result.onSuccess {
Toast.makeText(this, "Profile updated successfully!", Toast.LENGTH_SHORT).show()
}
result.onFailure { exception ->
Toast.makeText(this, "Error: ${exception.message}", Toast.LENGTH_LONG).show()
}
}
}
}
How does the logout work?
The viewModel.signOut() call clears the Firebase Authentication session so the user is no longer authenticated.
The intent flags FLAG_ACTIVITY_NEW_TASK and FLAG_ACTIVITY_CLEAR_TASK clear the entire activity back stack. This prevents the user from pressing the Back button to return to the Profile screen without signing in again.
- Save ProfileActivity.kt.
- Run the app on your emulator by clicking the green play button in the toolbar.
- Sign up with a new account (or sign in with an existing one). You should land on the Profile screen with your username, email, and phone number displayed.
- Edit one of the fields and tap Save Changes. You should see a toast message saying "Profile updated successfully!".
- Tap Logout. You should be taken back to the Sign In screen.
Profile fields are empty?
If the Profile screen loads with blank fields, sign out and sign in again. The sign-in flow fetches data from Firestore and caches it in Room. If you previously signed up before the ProfileViewModel was created, the Room cache may not have been populated correctly.
Help me debug why loadUser is not returning data from Room
Now verify that Room keeps user data cached locally even after logout.
- In Android Studio, click View in the top menu bar.
- Select Tool Windows, then App Inspection.
- Click the Database Inspector tab in the App Inspection panel.
- Select your running app process from the dropdown.
- Expand app_database and click on user_table.
- You should see a row with your user data (uid, username, email, phoneNumber) still present even though you logged out.
Why does Room keep data after logout?
Room is a local database on the device. Signing out only clears the Firebase Authentication session. The cached user record stays in Room so the app can display it instantly the next time the user signs in, without waiting for a network request.
✔️ Awesome, I've got everything!
Great. Make sure you have saved all three new files: activity_profile.xml, ProfileViewModel.kt, and ProfileActivity.kt.
ⓧ I'd like to double check the full code
Here is the full code for each new file created in this step.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="24dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="My Profile"
android:textSize="28sp"
android:textStyle="bold"
android:layout_marginBottom="32dp" />
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etUsername"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Username"
android:inputType="text" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etEmail"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Email"
android:inputType="textEmailAddress" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="24dp">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etPhone"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Phone Number"
android:inputType="phone" />
</com.google.android.material.textfield.TextInputLayout>
<Button
android:id="@+id/btnSave"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Save Changes"
android:layout_marginBottom="16dp" />
<Button
android:id="@+id/btnLogout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Logout"
style="@style/Widget.MaterialComponents.Button.OutlinedButton" />
</LinearLayout>
package com.example.databasedtest.viewmodel
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import com.example.databasedtest.data.local.database.AppDatabase
import com.example.databasedtest.data.local.entity.User
import com.example.databasedtest.data.repository.UserRepository
import kotlinx.coroutines.launch
class ProfileViewModel(application: Application) : AndroidViewModel(application) {
private val repository: UserRepository
private val _user = MutableLiveData<User?>()
val user: LiveData<User?> = _user
private val _updateResult = MutableLiveData<Result<Unit>>()
val updateResult: LiveData<Result<Unit>> = _updateResult
init {
val userDao = AppDatabase.getDatabase(application).userDao()
repository = UserRepository(userDao)
}
fun loadUser() {
viewModelScope.launch {
val uid = repository.getCurrentUserId()
if (uid != null) {
val user = repository.getUserFromRoom(uid)
_user.value = user
}
}
}
fun updateProfile(username: String, email: String, phoneNumber: String) {
viewModelScope.launch {
val uid = repository.getCurrentUserId() ?: return@launch
val updatedUser = User(uid = uid, username = username, email = email, phoneNumber = phoneNumber)
val result = repository.updateUserProfile(updatedUser)
_updateResult.value = result
if (result.isSuccess) {
_user.value = updatedUser
}
}
}
fun signOut() {
repository.signOut()
}
}
package com.example.databasedtest.ui
import android.content.Intent
import android.os.Bundle
import android.widget.Toast
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import com.example.databasedtest.databinding.ActivityProfileBinding
import com.example.databasedtest.viewmodel.ProfileViewModel
class ProfileActivity : AppCompatActivity() {
private lateinit var binding: ActivityProfileBinding
private val viewModel: ProfileViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityProfileBinding.inflate(layoutInflater)
setContentView(binding.root)
viewModel.loadUser()
viewModel.user.observe(this) { user ->
user?.let {
binding.etUsername.setText(it.username)
binding.etEmail.setText(it.email)
binding.etPhone.setText(it.phoneNumber)
}
}
binding.btnSave.setOnClickListener {
val username = binding.etUsername.text.toString().trim()
val email = binding.etEmail.text.toString().trim()
val phone = binding.etPhone.text.toString().trim()
if (username.isEmpty() || email.isEmpty() || phone.isEmpty()) {
Toast.makeText(this, "All fields are required", Toast.LENGTH_SHORT).show()
return@setOnClickListener
}
viewModel.updateProfile(username, email, phone)
}
binding.btnLogout.setOnClickListener {
viewModel.signOut()
val intent = Intent(this, SignInActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
startActivity(intent)
finish()
}
viewModel.updateResult.observe(this) { result ->
result.onSuccess {
Toast.makeText(this, "Profile updated successfully!", Toast.LENGTH_SHORT).show()
}
result.onFailure { exception ->
Toast.makeText(this, "Error: ${exception.message}", Toast.LENGTH_LONG).show()
}
}
}
}
Your app is fully functional. Users can sign up, sign in, view and edit their profile, and log out. Next up, you will tackle the Secret Mission and add account deletion across all three systems.
Secret mission
Add Delete Account Functionality
Your app lets users sign up, sign in, and edit their profile. But what happens when a user wants to leave? In production apps, account deletion is a legal requirement in many regions. Can you coordinate deletion across Firebase Auth, Firestore, and Room Database simultaneously?
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely on Firebase's free Spark plan, so there are no ongoing costs. Decide whether to keep your resources running or delete them entirely.
Resources you used:
- Firebase project "DatabasedTest" with Authentication and Firestore enabled (free Spark plan).
- Cloud Firestore database with user documents (free: 50,000 reads/day, 20,000 writes/day, 1 GiB storage).
- Firebase Authentication users (free: 3,000 daily active users).
- Local project files at C:\Users\Asus\Desktop\TTTM2213\databasedtest.
Keep everything running
No action needed. Choose this if you are still actively building or want to keep the project available for reference.
- Your Firebase project stays active on the free Spark plan indefinitely. No charges will occur with normal learning usage.
- No payment information is attached to the Spark plan.
- Your local project files remain on your machine and take up minimal disk space.
Pause - I'll come back to this later
There is nothing to pause for this project. Firebase Spark plan resources are free and do not consume compute when idle. Your local project files sit on disk without running any processes.
- Stop the emulator in Android Studio if it is still running.
- Close Android Studio to free up system memory.
- Everything else stays as-is until you return.
Delete - I don't want to use this again
Remove all project resources and start fresh. This permanently deletes your Firebase data and local files.
- Delete your Firebase project by opening the Firebase Console at console.firebase.google.com.
- Click the gear icon next to Project Overview and select Project settings.
- Scroll to the bottom of the General tab.
- Click Delete project.
- Check each acknowledgement checkbox and confirm the deletion. This removes Authentication users, Firestore data, and all other Firebase resources permanently.
- Delete your local project files by opening a terminal and running the command below.
rmdir /s /q C:\Users\Asus\Desktop\TTTM2213\databasedtest
What about the Google account?
Deleting the Firebase project does not delete your Google account. Your Google account is a general-purpose account used across many services. Keep it unless you have a specific reason to remove it.
Nice Work!
Nice Work!
You just built a complete user management Android app with dual-database storage, authentication flows, and proper MVVM architecture.
You've learned how to:
- Implement Firebase Authentication with email/password sign-up and sign-in, connected to a real Firebase project.
- Store user profile data in both Cloud Firestore (cloud) and Room Database (local) with synchronization between them using the Repository pattern.
- Use the Database Inspector to verify Room Database records in real time and debug your local data layer.
- Secret Mission: Added a Delete Account feature that coordinates deletion across Firebase Auth, Firestore, and Room Database with proper error handling.
Ready to quiz yourself?