Build a Classroom App with Firebase
Build an Android app with Firebase Auth and Firestore for class management.
Introduction
30 Second Summary
Every classroom needs a way to bring teachers and students together. Mobile apps make this possible from anywhere, letting people create learning spaces and join them with a simple code.
In this project, you will build an Android app called "Mobile Learning Best Buddy" where users can register, log in, create classes as a teacher, or join existing classes as a student using a class code.
What You'll Build
A classroom management Android app with five screens, powered by Firebase Authentication for user accounts and Cloud Firestore for storing class data.
By the end of this project, you'll have:
- A working Android app with email/password registration and login powered by Firebase Authentication.
- A Firestore-backed class system where users can create classes (becoming a teacher) or join classes (becoming a student).
- A multi-screen Compose app with navigation between Login, Registration, Dashboard, Create Class, and Join Class screens using Navigation Compose.
- Secret Mission: Build a "My Classes" screen that displays all enrolled classes with role badges and member counts.
Are there any prerequisites?
You'll need JDK 17 installed and a basic understanding of programming concepts. No prior Android or Kotlin experience is required.
Set Up Your Android Project and Firebase
Every Android app starts as a project in Android Studio. Before you can write any screens or connect to a backend, you need a properly configured project with the right dependencies.
In this step, you will create your Android project, set up Firebase as the backend, and configure all the libraries your app needs to run.
In this step, get ready to:
- Create a new Android Studio project with Jetpack Compose.
- Set up a Firebase project with Authentication and Firestore.
- Configure your build files with all required dependencies.
Create a new Android Studio project
You will create a fresh project using the Empty Activity template, which gives you a minimal Jetpack Compose setup to build on.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Android Studio and press Enter to open it.
- On the Welcome screen, click New Project.
- In the Templates pane, select Phone and Tablet on the left.
- Choose Empty Activity and click Next.
You will now see the project configuration screen.
- Set Name to Mobile Learning Best Buddy.
- Set Package name to com.example.mobilelearningbestbuddy.
- Leave the Save location as the default.
- Set Minimum SDK to API 26.
- Confirm that Build configuration language is set to Kotlin DSL.
- Click Finish.
Android Studio will generate your project and run an initial Gradle sync. Wait for the sync to complete before moving on.
What is Gradle sync?
Gradle is the build system that compiles your Android app. When you create or modify a project, Android Studio "syncs" with Gradle to download dependencies and configure everything. You will see a progress bar at the bottom of the screen while it runs.
Sync taking a very long time?
The first sync for a new project downloads many libraries and can take a few minutes depending on your internet speed. If it appears stuck for more than 5 minutes, check your internet connection and try clicking File then Sync Project with Gradle Files.
Help me troubleshoot my Gradle sync issue in Android Studio
Set up Firebase for your app
Firebase provides the backend services your app needs. You will use Firebase Authentication to manage user accounts and Cloud Firestore to store class data.
- Open your web browser and go to the Firebase Console.
- Sign in with your Google account.
- Click Add project.
- Enter a project name (for example, Mobile Learning Best Buddy).
- Disable Google Analytics (not needed for this project) and click Create project.
- Wait for the project to be created, then click Continue.
Now register your Android app with this Firebase project.
- On the project overview page, click the Android icon to add an Android app.
- In the Android package name field, enter com.example.mobilelearningbestbuddy.
- Click Register app.
- Click Download google-services.json to download the configuration file.
- Move the downloaded google-services.json file into your project's app/ directory (the same folder that contains your app-level build.gradle.kts).
- Back in the Firebase Console, click Next through the remaining setup steps until you reach the project overview.
Finding the app/ directory
In Android Studio, switch the project view to Project (not "Android") using the dropdown at the top of the left sidebar. You will see the app/ folder directly. Drag and drop google-services.json into it.
Next, enable Email/Password authentication.
- In the Firebase Console left sidebar, click Build, then select Authentication.
- Click Get started.
- Open the Sign-in method tab.
- Click Email/Password from the list of providers.
- Toggle the Enable switch to on and click Save.
Finally, create the Firestore database.
- In the Firebase Console left sidebar, click Build, then select Firestore Database.
- Click Create database.
- Select a database location closest to you and click Next.
- Under security rules, select Start in test mode and click Create.
What is test mode?
Test mode allows open read and write access to your database for 30 days. This is perfect for development. You would configure stricter security rules before releasing a production app.
Configure your project dependencies
Your Android project needs libraries for Compose UI, navigation, and Firebase. You will configure both the project-level and app-level build.gradle.kts files to declare these dependencies.
- In Android Studio, switch the project view dropdown (top of the left sidebar) to Project if it is not already.
- Open the project-level build.gradle.kts file (the one in the root of your project, NOT inside the app/ folder).
- Replace its entire contents with the following code:
plugins {
id("com.android.application") version "9.2.0" apply false
id("com.android.library") version "9.2.0" apply false
id("org.jetbrains.kotlin.android") version "2.3.21" apply false
id("org.jetbrains.kotlin.plugin.compose") version "2.3.21" apply false
id("com.google.gms.google-services") version "4.5.0" apply false
}
What does this code do?
This file declares the Gradle plugins your project uses. Each plugin is listed with apply false because they are applied individually in the app-level file.
- com.android.application and com.android.library are the core Android build plugins.
- org.jetbrains.kotlin.android adds Kotlin language support.
- org.jetbrains.kotlin.plugin.compose enables the Compose compiler.
- com.google.gms.google-services connects your app to Firebase using the google-services.json file.
- Save the project-level build.gradle.kts.
- You will not see a visible change yet. The app-level file in the next chunk uses these plugins.
Now configure the app-level build file with all the libraries your app needs.
- Open the app-level build.gradle.kts file (inside the app/ folder).
- Replace its entire contents with the following code:
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("org.jetbrains.kotlin.plugin.compose")
id("com.google.gms.google-services")
}
android {
namespace = "com.example.mobilelearningbestbuddy"
compileSdk = 35
defaultConfig {
applicationId = "com.example.mobilelearningbestbuddy"
minSdk = 26
targetSdk = 35
versionCode = 1
versionName = "1.0"
}
buildFeatures {
compose = true
}
}
dependencies {
val composeBom = platform("androidx.compose:compose-bom:2026.06.00")
implementation(composeBom)
androidTestImplementation(composeBom)
implementation("androidx.compose.material3:material3")
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-tooling-preview")
debugImplementation("androidx.compose.ui:ui-tooling")
implementation("androidx.activity:activity-compose:1.13.0")
implementation("androidx.navigation:navigation-compose:2.9.8")
implementation(platform("com.google.firebase:firebase-bom:34.15.0"))
implementation("com.google.firebase:firebase-auth")
implementation("com.google.firebase:firebase-firestore")
}
What does this code do?
This file configures your app's build settings and declares the libraries it depends on.
- The plugins block applies each plugin declared in the project-level file.
- compileSdk = 35 and targetSdk = 35 target Android 15.
- The Compose BOM (2026.06.00) manages compatible versions of all Compose libraries.
- navigation-compose lets you navigate between screens.
- The Firebase BOM (34.15.0) manages compatible versions of firebase-auth and firebase-firestore.
- Save the app-level build.gradle.kts.
- Android Studio will display a banner at the top saying the Gradle files have changed. Click Sync Now to sync the project.
- Wait for the sync to finish. You should see "BUILD SUCCESSFUL" or a green checkmark in the Build window at the bottom.
Seeing a sync error?
If you see an error mentioning the missing google-services.json file, make sure you placed the file directly inside the app/ folder (not in a subfolder).
If you see a dependency resolution error, check that your internet connection is active. Gradle needs to download libraries from the internet during sync.
Help me fix my Gradle sync error in Android Studio
✔️ Awesome, I've got everything!
Great. Double check that both files are saved and the sync completed successfully.
ⓧ I'd like to double check the full code
Here is the complete project-level build.gradle.kts:
plugins {
id("com.android.application") version "9.2.0" apply false
id("com.android.library") version "9.2.0" apply false
id("org.jetbrains.kotlin.android") version "2.3.21" apply false
id("org.jetbrains.kotlin.plugin.compose") version "2.3.21" apply false
id("com.google.gms.google-services") version "4.5.0" apply false
}
Here is the complete app-level build.gradle.kts:
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("org.jetbrains.kotlin.plugin.compose")
id("com.google.gms.google-services")
}
android {
namespace = "com.example.mobilelearningbestbuddy"
compileSdk = 35
defaultConfig {
applicationId = "com.example.mobilelearningbestbuddy"
minSdk = 26
targetSdk = 35
versionCode = 1
versionName = "1.0"
}
buildFeatures {
compose = true
}
}
dependencies {
val composeBom = platform("androidx.compose:compose-bom:2026.06.00")
implementation(composeBom)
androidTestImplementation(composeBom)
implementation("androidx.compose.material3:material3")
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-tooling-preview")
debugImplementation("androidx.compose.ui:ui-tooling")
implementation("androidx.activity:activity-compose:1.13.0")
implementation("androidx.navigation:navigation-compose:2.9.8")
implementation(platform("com.google.firebase:firebase-bom:34.15.0"))
implementation("com.google.firebase:firebase-auth")
implementation("com.google.firebase:firebase-firestore")
}
Your project is fully configured with Firebase and Compose. Next up, you will build the registration and login screens that let users create accounts and sign in.
Build the Registration and Login Screens
Your project is configured with all the dependencies it needs. Now it's time to build the screens your users will actually see and interact with.
Every app needs a front door. In this step, you'll build two screens: one for logging in and one for registering a new account. Both screens will connect directly to Firebase Authentication to handle user credentials securely.
In this step, get ready to:
- Create a LoginScreen composable with email and password fields.
- Create a RegisterScreen composable with name, email, password, and confirm password fields.
- Wire up Firebase Authentication to handle sign-in and registration logic.
Create the login screen
The login screen is the first thing users see when they open your app. It needs email and password fields, a login button that authenticates with Firebase, and a link to the registration screen for new users.
- In Android Studio, find the Project panel on the left side.
- Right-click on the com.example.mobilelearningbestbuddy package.
- Select New > Package.
- Name the new package screens and press Enter.
- Right-click on the new screens package.
- Select New > Kotlin Class/File.
- Choose File, name it LoginScreen, and press Enter.
- Replace any existing content in LoginScreen.kt with the following code:
package com.example.mobilelearningbestbuddy.screens
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.unit.dp
import com.google.firebase.Firebase
import com.google.firebase.auth.auth
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun LoginScreen(
onNavigateToRegister: () -> Unit,
onLoginSuccess: () -> Unit
) {
// State variables to hold user input and UI state
var email by remember { mutableStateOf("") }
var password by remember { mutableStateOf("") }
var isLoading by remember { mutableStateOf(false) }
var errorMessage by remember { mutableStateOf<String?>(null) }
Column(
modifier = Modifier
.fillMaxSize()
.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
// App title and subtitle
Text(
text = "Mobile Learning Best Buddy",
style = MaterialTheme.typography.headlineMedium
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "Sign in to continue",
style = MaterialTheme.typography.bodyLarge
)
Spacer(modifier = Modifier.height(32.dp))
// Email input field
OutlinedTextField(
value = email,
onValueChange = { email = it },
label = { Text("Email") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email),
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(16.dp))
// Password input field with hidden characters
OutlinedTextField(
value = password,
onValueChange = { password = it },
label = { Text("Password") },
visualTransformation = PasswordVisualTransformation(),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(24.dp))
// Display error message if login fails
if (errorMessage != null) {
Text(
text = errorMessage!!,
color = MaterialTheme.colorScheme.error,
style = MaterialTheme.typography.bodySmall
)
Spacer(modifier = Modifier.height(8.dp))
}
// Login button that calls Firebase Authentication
Button(
onClick = {
isLoading = true
errorMessage = null
Firebase.auth.signInWithEmailAndPassword(email, password)
.addOnCompleteListener { task ->
isLoading = false
if (task.isSuccessful) {
onLoginSuccess()
} else {
errorMessage = task.exception?.message ?: "Login failed"
}
}
},
modifier = Modifier.fillMaxWidth(),
enabled = !isLoading
) {
if (isLoading) {
CircularProgressIndicator(modifier = Modifier.size(20.dp))
} else {
Text("Login")
}
}
Spacer(modifier = Modifier.height(16.dp))
// Link to navigate to the registration screen
TextButton(onClick = onNavigateToRegister) {
Text("Don't have an account? Register")
}
}
}
What does this code do?
This composable function builds the entire login screen using Material 3 components.
- The remember { mutableStateOf() } calls create state variables that survive screen recompositions. When the user types, the UI updates automatically.
- OutlinedTextField provides Material 3 styled input fields. The password field uses PasswordVisualTransformation() to hide characters.
- Firebase.auth.signInWithEmailAndPassword() sends the credentials to Firebase. The addOnCompleteListener callback handles success or failure.
- The isLoading state swaps the button text for a spinning indicator while Firebase processes the request.
- Save LoginScreen.kt by pressing Cmd+S (macOS) or Ctrl+S (Windows).
- Confirm there are no red underlines or error highlights in the editor. If the file compiles cleanly, your imports and syntax are correct.
Seeing red underlines on Firebase imports?
Make sure your import lines say com.google.firebase.Firebase and com.google.firebase.auth.auth. These are the current import paths for Firebase (the older KTX imports no longer work).
If you still see errors, try clicking File > Sync Project with Gradle Files to re-sync your dependencies.
Still stuck? Help me fix Firebase import errors in LoginScreen.kt
Create the registration screen
The registration screen collects a new user's name, email, and password. It also confirms the password to catch typos before submitting to Firebase.
- Right-click on the screens package in the Project panel.
- Select New > Kotlin Class/File.
- Choose File, name it RegisterScreen, and press Enter.
- Replace any existing content in RegisterScreen.kt with the following code:
package com.example.mobilelearningbestbuddy.screens
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.unit.dp
import com.google.firebase.Firebase
import com.google.firebase.auth.auth
import com.google.firebase.firestore.firestore
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun RegisterScreen(
onNavigateToLogin: () -> Unit,
onRegisterSuccess: () -> Unit
) {
// State variables for form fields and UI feedback
var name by remember { mutableStateOf("") }
var email by remember { mutableStateOf("") }
var password by remember { mutableStateOf("") }
var confirmPassword by remember { mutableStateOf("") }
var isLoading by remember { mutableStateOf(false) }
var errorMessage by remember { mutableStateOf<String?>(null) }
Column(
modifier = Modifier
.fillMaxSize()
.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
// Screen title
Text(
text = "Create Account",
style = MaterialTheme.typography.headlineMedium
)
Spacer(modifier = Modifier.height(32.dp))
// Full name input
OutlinedTextField(
value = name,
onValueChange = { name = it },
label = { Text("Full Name") },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(16.dp))
// Email input
OutlinedTextField(
value = email,
onValueChange = { email = it },
label = { Text("Email") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email),
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(16.dp))
// Password input
OutlinedTextField(
value = password,
onValueChange = { password = it },
label = { Text("Password") },
visualTransformation = PasswordVisualTransformation(),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(16.dp))
// Confirm password input
OutlinedTextField(
value = confirmPassword,
onValueChange = { confirmPassword = it },
label = { Text("Confirm Password") },
visualTransformation = PasswordVisualTransformation(),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(24.dp))
// Display error message if registration fails
if (errorMessage != null) {
Text(
text = errorMessage!!,
color = MaterialTheme.colorScheme.error,
style = MaterialTheme.typography.bodySmall
)
Spacer(modifier = Modifier.height(8.dp))
}
// Register button with Firebase Auth and Firestore logic
Button(
onClick = {
if (password != confirmPassword) {
errorMessage = "Passwords do not match"
return@Button
}
isLoading = true
errorMessage = null
Firebase.auth.createUserWithEmailAndPassword(email, password)
.addOnCompleteListener { task ->
if (task.isSuccessful) {
val uid = Firebase.auth.currentUser?.uid ?: return@addOnCompleteListener
val userData = hashMapOf(
"name" to name,
"email" to email
)
Firebase.firestore.collection("users").document(uid)
.set(userData)
.addOnCompleteListener {
isLoading = false
onRegisterSuccess()
}
} else {
isLoading = false
errorMessage = task.exception?.message ?: "Registration failed"
}
}
},
modifier = Modifier.fillMaxWidth(),
enabled = !isLoading
) {
if (isLoading) {
CircularProgressIndicator(modifier = Modifier.size(20.dp))
} else {
Text("Register")
}
}
Spacer(modifier = Modifier.height(16.dp))
// Link to navigate back to login
TextButton(onClick = onNavigateToLogin) {
Text("Already have an account? Login")
}
}
}
What does this code do?
This composable builds the registration form and connects it to both Firebase Authentication and Cloud Firestore.
- The password confirmation check runs locally before calling Firebase. If password != confirmPassword, the function returns early with an error message.
- Firebase.auth.createUserWithEmailAndPassword() creates the user account in Firebase Authentication.
- After the account is created, Firebase.firestore.collection("users").document(uid).set(userData) saves the user's name and email to a Firestore document. This gives you a place to store extra profile data beyond what Firebase Auth holds.
- The return@Button and return@addOnCompleteListener are Kotlin labeled returns that exit specific lambda scopes without crashing the app.
- Save RegisterScreen.kt by pressing Cmd+S (macOS) or Ctrl+S (Windows).
- Confirm there are no red underlines in the editor. Both com.google.firebase.auth.auth and com.google.firebase.firestore.firestore should resolve cleanly.
Seeing an error on the Firestore import?
The correct import is com.google.firebase.firestore.firestore (lowercase firestore at the end). Make sure you did not accidentally import a KTX variant.
If all imports look correct but errors remain, click File > Invalidate Caches / Restart and let Android Studio re-index.
Need more help? Help me fix Firestore import errors in RegisterScreen.kt
Understand the Firebase Authentication flow
Both screens now contain Firebase Authentication logic embedded in their button handlers. Let's break down the two distinct flows you just built.
How does the login flow work?
When the user taps Login, three things happen in sequence:
- isLoading flips to true, which disables the button and shows a spinner. This prevents double-taps.
- signInWithEmailAndPassword() sends the email and password to Firebase's servers for verification.
- The addOnCompleteListener callback either triggers onLoginSuccess() (which will navigate to the dashboard later) or displays the error message from Firebase.
How does the registration flow work?
Registration has an extra step compared to login. After createUserWithEmailAndPassword() succeeds, the code also writes a document to Firestore.
The document goes into a users collection, keyed by the user's UID. It stores their name and email so you can display personalized content on the dashboard later.
Why store user data in Firestore separately?
Firebase Authentication only stores an email and password. It does not store custom profile fields like a display name or role. By writing to Firestore immediately after registration, you create a profile document you can extend later with class memberships, preferences, or any other data your app needs.
✔️ Awesome, I've got everything!
Great. Double check you have saved both files.
ⓧ I'd like to double check the full code
Here is the full LoginScreen.kt:
package com.example.mobilelearningbestbuddy.screens
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.unit.dp
import com.google.firebase.Firebase
import com.google.firebase.auth.auth
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun LoginScreen(
onNavigateToRegister: () -> Unit,
onLoginSuccess: () -> Unit
) {
var email by remember { mutableStateOf("") }
var password by remember { mutableStateOf("") }
var isLoading by remember { mutableStateOf(false) }
var errorMessage by remember { mutableStateOf<String?>(null) }
Column(
modifier = Modifier
.fillMaxSize()
.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
text = "Mobile Learning Best Buddy",
style = MaterialTheme.typography.headlineMedium
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "Sign in to continue",
style = MaterialTheme.typography.bodyLarge
)
Spacer(modifier = Modifier.height(32.dp))
OutlinedTextField(
value = email,
onValueChange = { email = it },
label = { Text("Email") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email),
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(16.dp))
OutlinedTextField(
value = password,
onValueChange = { password = it },
label = { Text("Password") },
visualTransformation = PasswordVisualTransformation(),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(24.dp))
if (errorMessage != null) {
Text(
text = errorMessage!!,
color = MaterialTheme.colorScheme.error,
style = MaterialTheme.typography.bodySmall
)
Spacer(modifier = Modifier.height(8.dp))
}
Button(
onClick = {
isLoading = true
errorMessage = null
Firebase.auth.signInWithEmailAndPassword(email, password)
.addOnCompleteListener { task ->
isLoading = false
if (task.isSuccessful) {
onLoginSuccess()
} else {
errorMessage = task.exception?.message ?: "Login failed"
}
}
},
modifier = Modifier.fillMaxWidth(),
enabled = !isLoading
) {
if (isLoading) {
CircularProgressIndicator(modifier = Modifier.size(20.dp))
} else {
Text("Login")
}
}
Spacer(modifier = Modifier.height(16.dp))
TextButton(onClick = onNavigateToRegister) {
Text("Don't have an account? Register")
}
}
}
And here is the full RegisterScreen.kt:
package com.example.mobilelearningbestbuddy.screens
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.unit.dp
import com.google.firebase.Firebase
import com.google.firebase.auth.auth
import com.google.firebase.firestore.firestore
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun RegisterScreen(
onNavigateToLogin: () -> Unit,
onRegisterSuccess: () -> Unit
) {
var name by remember { mutableStateOf("") }
var email by remember { mutableStateOf("") }
var password by remember { mutableStateOf("") }
var confirmPassword by remember { mutableStateOf("") }
var isLoading by remember { mutableStateOf(false) }
var errorMessage by remember { mutableStateOf<String?>(null) }
Column(
modifier = Modifier
.fillMaxSize()
.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
text = "Create Account",
style = MaterialTheme.typography.headlineMedium
)
Spacer(modifier = Modifier.height(32.dp))
OutlinedTextField(
value = name,
onValueChange = { name = it },
label = { Text("Full Name") },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(16.dp))
OutlinedTextField(
value = email,
onValueChange = { email = it },
label = { Text("Email") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email),
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(16.dp))
OutlinedTextField(
value = password,
onValueChange = { password = it },
label = { Text("Password") },
visualTransformation = PasswordVisualTransformation(),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(16.dp))
OutlinedTextField(
value = confirmPassword,
onValueChange = { confirmPassword = it },
label = { Text("Confirm Password") },
visualTransformation = PasswordVisualTransformation(),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(24.dp))
if (errorMessage != null) {
Text(
text = errorMessage!!,
color = MaterialTheme.colorScheme.error,
style = MaterialTheme.typography.bodySmall
)
Spacer(modifier = Modifier.height(8.dp))
}
Button(
onClick = {
if (password != confirmPassword) {
errorMessage = "Passwords do not match"
return@Button
}
isLoading = true
errorMessage = null
Firebase.auth.createUserWithEmailAndPassword(email, password)
.addOnCompleteListener { task ->
if (task.isSuccessful) {
val uid = Firebase.auth.currentUser?.uid ?: return@addOnCompleteListener
val userData = hashMapOf(
"name" to name,
"email" to email
)
Firebase.firestore.collection("users").document(uid)
.set(userData)
.addOnCompleteListener {
isLoading = false
onRegisterSuccess()
}
} else {
isLoading = false
errorMessage = task.exception?.message ?: "Registration failed"
}
}
},
modifier = Modifier.fillMaxWidth(),
enabled = !isLoading
) {
if (isLoading) {
CircularProgressIndicator(modifier = Modifier.size(20.dp))
} else {
Text("Register")
}
}
Spacer(modifier = Modifier.height(16.dp))
TextButton(onClick = onNavigateToLogin) {
Text("Already have an account? Login")
}
}
}
Your login and registration screens are ready. Next up, you'll build the dashboard screen that users see after they sign in, complete with navigation to create and join classes.
Build the Dashboard Screen
Your login and registration screens are ready. Users can create accounts and sign in, but after a successful login they need somewhere to land.
In this step, you'll build the Dashboard screen. This is the home base of the app where users see a personalized welcome message and choose their next action: creating a class or joining one.
In this step, get ready to:
- Create the DashboardScreen composable with a welcome message and action cards.
- Add a top app bar with a logout button.
- Wire up navigation callbacks for class creation and joining.
Create the DashboardScreen composable
The Dashboard is what users see after logging in. It fetches their name from Cloud Firestore to display a personalized greeting, and presents two clickable cards for the app's main actions.
- In Android Studio, right-click the screens package (inside com.example.mobilelearningbestbuddy).
- Select New, then Kotlin Class/File.
- Enter DashboardScreen as the name and select File.
- Replace any content in the new file with the following code:
package com.example.mobilelearningbestbuddy.screens
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ExitToApp
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.google.firebase.Firebase
import com.google.firebase.auth.auth
import com.google.firebase.firestore.firestore
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DashboardScreen(
onNavigateToCreateClass: () -> Unit,
onNavigateToJoinClass: () -> Unit,
onLogout: () -> Unit
) {
// State variable to hold the user's name from Firestore
var userName by remember { mutableStateOf("") }
// Fetch the user's name from the "users" collection when the screen loads
LaunchedEffect(Unit) {
val uid = Firebase.auth.currentUser?.uid ?: return@LaunchedEffect
Firebase.firestore.collection("users").document(uid).get()
.addOnSuccessListener { document ->
userName = document.getString("name") ?: "User"
}
}
Scaffold(
topBar = {
TopAppBar(
title = { Text("Mobile Learning Best Buddy") },
actions = {
// Logout button in the top right corner
IconButton(onClick = onLogout) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ExitToApp,
contentDescription = "Logout"
)
}
}
)
}
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
// Personalized welcome message using the fetched name
Text(
text = "Welcome, $userName!",
style = MaterialTheme.typography.headlineSmall
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "What would you like to do?",
style = MaterialTheme.typography.bodyLarge
)
Spacer(modifier = Modifier.height(32.dp))
// Action card for creating a new class
Card(
onClick = onNavigateToCreateClass,
modifier = Modifier.fillMaxWidth()
) {
Column(modifier = Modifier.padding(24.dp)) {
Text(
text = "Create a Class",
style = MaterialTheme.typography.titleMedium
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = "Start teaching by creating a new class",
style = MaterialTheme.typography.bodyMedium
)
}
}
Spacer(modifier = Modifier.height(16.dp))
// Action card for joining an existing class
Card(
onClick = onNavigateToJoinClass,
modifier = Modifier.fillMaxWidth()
) {
Column(modifier = Modifier.padding(24.dp)) {
Text(
text = "Join a Class",
style = MaterialTheme.typography.titleMedium
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = "Enter a class code to join as a student",
style = MaterialTheme.typography.bodyMedium
)
}
}
}
}
}
What does this code do?
This composable builds the main hub of your app. Here's what each section handles:
- LaunchedEffect(Unit) runs once when the screen appears. It grabs the logged-in user's UID and fetches their name from the Firestore users collection.
- The Scaffold provides the Material 3 screen structure with a TopAppBar showing the app title and a logout icon.
- Two Card composables act as large, tappable buttons. Each one triggers a navigation callback when clicked.
- Save DashboardScreen.kt.
- You won't see this screen in the emulator yet because it hasn't been connected to the navigation graph. That happens in the next step.
Seeing red underlines in the file?
- Check that the package declaration at the top reads com.example.mobilelearningbestbuddy.screens. A mismatch with your folder structure causes unresolved reference errors.
- Make sure the Material Icons dependency resolved correctly. If Icons.AutoMirrored.Filled.ExitToApp is red, click File then Sync Project with Gradle Files.
Help me fix import errors in my DashboardScreen composable.
Understand the top app bar and logout
The Scaffold composable is the standard Material 3 layout wrapper. It reserves space for a top bar, bottom bar, and floating action buttons so your content never overlaps with them.
How does the logout button work?
The IconButton in the TopAppBar's actions slot calls onLogout when tapped. In the next step, you'll connect this callback so it calls Firebase.auth.signOut() and redirects back to the login screen.
The icon uses Icons.AutoMirrored.Filled.ExitToApp which automatically flips direction for right-to-left languages. This is a best practice for accessibility.
Wire up navigation with callback parameters
Notice that DashboardScreen accepts three lambda parameters: onNavigateToCreateClass, onNavigateToJoinClass, and onLogout. These are the screen's navigation hooks.
Why use callbacks instead of a NavController?
Passing a NavController directly into a composable couples it to the navigation library. By using callback lambdas instead, the screen stays reusable and testable.
When you build the navigation graph in the next step, you'll pass in lambdas like { navController.navigate("createClass") } to connect each callback to an actual route.
- Look at the two Card composables in your code. The first one has onClick = onNavigateToCreateClass and the second has onClick = onNavigateToJoinClass.
- When a user taps either card, the corresponding callback fires and the navigation layer handles the screen transition.
✔️ Awesome, I've got everything!
Great. Double check you've saved DashboardScreen.kt and that there are no red underlines in the file.
ⓧ I'd like to double check the full code
Here is the complete DashboardScreen.kt file for comparison:
package com.example.mobilelearningbestbuddy.screens
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ExitToApp
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.google.firebase.Firebase
import com.google.firebase.auth.auth
import com.google.firebase.firestore.firestore
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DashboardScreen(
onNavigateToCreateClass: () -> Unit,
onNavigateToJoinClass: () -> Unit,
onLogout: () -> Unit
) {
var userName by remember { mutableStateOf("") }
LaunchedEffect(Unit) {
val uid = Firebase.auth.currentUser?.uid ?: return@LaunchedEffect
Firebase.firestore.collection("users").document(uid).get()
.addOnSuccessListener { document ->
userName = document.getString("name") ?: "User"
}
}
Scaffold(
topBar = {
TopAppBar(
title = { Text("Mobile Learning Best Buddy") },
actions = {
IconButton(onClick = onLogout) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ExitToApp,
contentDescription = "Logout"
)
}
}
)
}
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Welcome, $userName!",
style = MaterialTheme.typography.headlineSmall
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "What would you like to do?",
style = MaterialTheme.typography.bodyLarge
)
Spacer(modifier = Modifier.height(32.dp))
Card(
onClick = onNavigateToCreateClass,
modifier = Modifier.fillMaxWidth()
) {
Column(modifier = Modifier.padding(24.dp)) {
Text(
text = "Create a Class",
style = MaterialTheme.typography.titleMedium
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = "Start teaching by creating a new class",
style = MaterialTheme.typography.bodyMedium
)
}
}
Spacer(modifier = Modifier.height(16.dp))
Card(
onClick = onNavigateToJoinClass,
modifier = Modifier.fillMaxWidth()
) {
Column(modifier = Modifier.padding(24.dp)) {
Text(
text = "Join a Class",
style = MaterialTheme.typography.titleMedium
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = "Enter a class code to join as a student",
style = MaterialTheme.typography.bodyMedium
)
}
}
}
}
}
Your Dashboard screen is ready to greet users by name and offer them two clear paths forward. Next up, you'll build the Create Class and Join Class screens, then connect everything together with a navigation graph.
Build Create Class, Join Class, and Wire Up Navigation
Your dashboard is ready and waiting for the learner to take action. The two cards you built ("Create a Class" and "Join a Class") point to screens that don't exist yet.
In this step, you'll build those two screens and connect every screen in your app together with a navigation graph. By the end, tapping any button will smoothly transition the user to the correct destination.
In this step, get ready to:
- Build a Create Class screen that writes class data to Firestore.
- Build a Join Class screen that lets students enter a class code.
- Wire up a full navigation graph connecting all five screens.
Build the CreateClassScreen composable
Teachers need a way to create a new class and share a unique code with their students. The CreateClassScreen collects a class name and subject, generates a random 6-character code, and saves everything to Cloud Firestore.
- In the screens package inside Android Studio, create a new Kotlin file called CreateClassScreen.kt.
- Paste the following code into the file:
package com.example.mobilelearningbestbuddy.screens
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.google.firebase.Firebase
import com.google.firebase.auth.auth
import com.google.firebase.firestore.firestore
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CreateClassScreen(
onClassCreated: () -> Unit,
onNavigateBack: () -> Unit
) {
var className by remember { mutableStateOf("") }
var subject by remember { mutableStateOf("") }
var isLoading by remember { mutableStateOf(false) }
var errorMessage by remember { mutableStateOf<String?>(null) }
Scaffold(
topBar = {
TopAppBar(
title = { Text("Create a Class") },
navigationIcon = {
IconButton(onClick = onNavigateBack) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "Back"
)
}
}
)
}
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.padding(24.dp)
) {
// Text field for the class name
OutlinedTextField(
value = className,
onValueChange = { className = it },
label = { Text("Class Name") },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(16.dp))
// Text field for the subject
OutlinedTextField(
value = subject,
onValueChange = { subject = it },
label = { Text("Subject") },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(24.dp))
// Display error message if present
if (errorMessage != null) {
Text(
text = errorMessage!!,
color = MaterialTheme.colorScheme.error,
style = MaterialTheme.typography.bodySmall
)
Spacer(modifier = Modifier.height(8.dp))
}
// Submit button that creates the class in Firestore
Button(
onClick = {
if (className.isBlank() || subject.isBlank()) {
errorMessage = "Please fill in all fields"
return@Button
}
isLoading = true
errorMessage = null
val currentUser = Firebase.auth.currentUser ?: return@Button
val classCode = generateClassCode()
val classData = hashMapOf(
"name" to className,
"subject" to subject,
"code" to classCode,
"teacherUid" to currentUser.uid,
"teacherName" to (currentUser.displayName ?: "Teacher"),
"members" to listOf(currentUser.uid)
)
Firebase.firestore.collection("classes")
.add(classData)
.addOnSuccessListener {
isLoading = false
onClassCreated()
}
.addOnFailureListener { e ->
isLoading = false
errorMessage = e.message ?: "Failed to create class"
}
},
modifier = Modifier.fillMaxWidth(),
enabled = !isLoading
) {
if (isLoading) {
CircularProgressIndicator(modifier = Modifier.size(20.dp))
} else {
Text("Create Class")
}
}
}
}
}
// Generates a random 6-character alphanumeric class code
private fun generateClassCode(): String {
val chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
return (1..6).map { chars.random() }.joinToString("")
}
What does this code do?
The CreateClassScreen composable collects a class name and subject through two OutlinedTextField inputs. When the teacher taps Create Class, the button's onClick handler validates the fields, generates a random code, and writes a new document to Firestore.
- generateClassCode() picks 6 random characters from A-Z and 0-9 to produce a shareable class code.
- The classData hashmap stores the class name, subject, code, teacher UID, teacher name, and a members array that starts with the teacher's UID.
- Firebase.firestore.collection("classes").add(classData) creates a new document in the classes collection with a unique auto-generated ID.
- Save CreateClassScreen.kt.
Seeing unresolved reference errors?
- Make sure the file's package declaration is com.example.mobilelearningbestbuddy.screens. A mismatched package causes import failures.
- Confirm that firebase-auth and firebase-firestore are in your build.gradle.kts dependencies and that you've synced the project.
Why is my CreateClassScreen showing unresolved references?
Build the JoinClassScreen composable
Students join a class by entering the 6-character code their teacher shares. The JoinClassScreen queries Firestore for a matching code and adds the student's UID to that class's members array.
- In the screens package, create a new Kotlin file called JoinClassScreen.kt.
- Paste the following code into the file:
package com.example.mobilelearningbestbuddy.screens
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.google.firebase.Firebase
import com.google.firebase.auth.auth
import com.google.firebase.firestore.FieldValue
import com.google.firebase.firestore.firestore
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun JoinClassScreen(
onClassJoined: () -> Unit,
onNavigateBack: () -> Unit
) {
var classCode by remember { mutableStateOf("") }
var isLoading by remember { mutableStateOf(false) }
var errorMessage by remember { mutableStateOf<String?>(null) }
Scaffold(
topBar = {
TopAppBar(
title = { Text("Join a Class") },
navigationIcon = {
IconButton(onClick = onNavigateBack) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "Back"
)
}
}
)
}
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.padding(24.dp)
) {
Text(
text = "Enter the class code provided by your teacher",
style = MaterialTheme.typography.bodyLarge
)
Spacer(modifier = Modifier.height(16.dp))
// Auto-uppercases the input so codes always match
OutlinedTextField(
value = classCode,
onValueChange = { classCode = it.uppercase() },
label = { Text("Class Code") },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(24.dp))
if (errorMessage != null) {
Text(
text = errorMessage!!,
color = MaterialTheme.colorScheme.error,
style = MaterialTheme.typography.bodySmall
)
Spacer(modifier = Modifier.height(8.dp))
}
// Button that queries Firestore for the class code
Button(
onClick = {
if (classCode.isBlank()) {
errorMessage = "Please enter a class code"
return@Button
}
isLoading = true
errorMessage = null
val currentUser = Firebase.auth.currentUser ?: return@Button
Firebase.firestore.collection("classes")
.whereEqualTo("code", classCode)
.get()
.addOnSuccessListener { documents ->
if (documents.isEmpty) {
isLoading = false
errorMessage = "Invalid class code. Please check and try again."
} else {
val classDoc = documents.documents[0]
classDoc.reference.update(
"members", FieldValue.arrayUnion(currentUser.uid)
).addOnSuccessListener {
isLoading = false
onClassJoined()
}.addOnFailureListener { e ->
isLoading = false
errorMessage = e.message ?: "Failed to join class"
}
}
}
.addOnFailureListener { e ->
isLoading = false
errorMessage = e.message ?: "Failed to find class"
}
},
modifier = Modifier.fillMaxWidth(),
enabled = !isLoading
) {
if (isLoading) {
CircularProgressIndicator(modifier = Modifier.size(20.dp))
} else {
Text("Join Class")
}
}
}
}
}
What does this code do?
The JoinClassScreen lets a student type a class code, then searches Firestore for a matching document.
- it.uppercase() in the text field's onValueChange forces the input to uppercase so the code always matches what was generated.
- .whereEqualTo("code", classCode) queries the classes collection for a document whose code field matches the entered value.
- FieldValue.arrayUnion(currentUser.uid) atomically adds the student's UID to the members array without duplicating it if they rejoin.
- Save JoinClassScreen.kt.
FieldValue import not resolving?
Make sure you import com.google.firebase.firestore.FieldValue at the top of the file. This is a separate import from com.google.firebase.firestore.firestore.
Why can't Android Studio find FieldValue in my JoinClassScreen?
Set up the navigation graph and update MainActivity
Right now your screens exist as isolated composables with no way to move between them. Navigation Compose provides a NavHost that maps string routes to composable destinations and handles the back stack automatically.
- In the Project panel, right-click your main package (com.example.mobilelearningbestbuddy) and select New then Package.
- Name the package navigation.
- Inside the new navigation package, create a Kotlin file called AppNavigation.kt.
- Paste the following code:
package com.example.mobilelearningbestbuddy.navigation
import androidx.compose.runtime.Composable
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.example.mobilelearningbestbuddy.screens.CreateClassScreen
import com.example.mobilelearningbestbuddy.screens.DashboardScreen
import com.example.mobilelearningbestbuddy.screens.JoinClassScreen
import com.example.mobilelearningbestbuddy.screens.LoginScreen
import com.example.mobilelearningbestbuddy.screens.RegisterScreen
import com.google.firebase.Firebase
import com.google.firebase.auth.auth
@Composable
fun AppNavigation() {
val navController = rememberNavController()
// If the user is already signed in, skip the login screen
val startDestination = if (Firebase.auth.currentUser != null) "dashboard" else "login"
NavHost(navController = navController, startDestination = startDestination) {
composable("login") {
LoginScreen(
onNavigateToRegister = { navController.navigate("register") },
onLoginSuccess = {
navController.navigate("dashboard") {
popUpTo("login") { inclusive = true }
}
}
)
}
composable("register") {
RegisterScreen(
onNavigateToLogin = { navController.popBackStack() },
onRegisterSuccess = {
navController.navigate("dashboard") {
popUpTo("login") { inclusive = true }
}
}
)
}
composable("dashboard") {
DashboardScreen(
onNavigateToCreateClass = { navController.navigate("createClass") },
onNavigateToJoinClass = { navController.navigate("joinClass") },
onLogout = {
Firebase.auth.signOut()
navController.navigate("login") {
popUpTo("dashboard") { inclusive = true }
}
}
)
}
composable("createClass") {
CreateClassScreen(
onClassCreated = { navController.popBackStack() },
onNavigateBack = { navController.popBackStack() }
)
}
composable("joinClass") {
JoinClassScreen(
onClassJoined = { navController.popBackStack() },
onNavigateBack = { navController.popBackStack() }
)
}
}
}
What does this code do?
AppNavigation sets up the full navigation graph for your app with five routes.
- rememberNavController() creates a navigation controller that tracks which screen the user is on.
- startDestination checks if the user is already signed in. If they are, the app opens to the dashboard instead of login.
- popUpTo("login") { inclusive = true } removes the login screen from the back stack after a successful sign-in so the user can't press Back to return to it.
- Each screen receives lambda callbacks (like onClassCreated) that call navController.popBackStack() or navController.navigate(...) to move between destinations.
- Save AppNavigation.kt.
Now you need to update MainActivity.kt to use AppNavigation as the app's root composable.
- Open MainActivity.kt and replace its entire contents with this code:
package com.example.mobilelearningbestbuddy
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.ui.Modifier
import com.example.mobilelearningbestbuddy.ui.theme.MobileLearningBestBuddyTheme
import com.example.mobilelearningbestbuddy.navigation.AppNavigation
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MobileLearningBestBuddyTheme {
// Surface applies the Material theme background color
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
// AppNavigation handles all screen routing
AppNavigation()
}
}
}
}
}
What changed in MainActivity?
Previously, MainActivity rendered a single screen directly. Now it calls AppNavigation() which decides which screen to show based on the current route. The Surface wrapper ensures the Material 3 background color applies to every screen.
- Save MainActivity.kt.
- Click the green Run button (or press Shift+F10) to build and run the app on your emulator.
- You should see the Login screen. Register a new account, then tap Create a Class on the Dashboard to verify the navigation works.
App crashes on launch?
- Check that AppNavigation is imported correctly in MainActivity.kt. The import should be com.example.mobilelearningbestbuddy.navigation.AppNavigation.
- Verify your google-services.json is in the app/ directory. A missing config file causes a Firebase initialization crash.
- Look at the Logcat panel in Android Studio for the exact error message. Filter by your app's package name.
My app crashes when I launch it after adding navigation. How do I fix this?
✔️ Awesome, I've got everything!
Great. Double check you've saved all files before moving on.
ⓧ I'd like to double check the full code
Here are all the files you created or modified in this step.
package com.example.mobilelearningbestbuddy.screens
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.google.firebase.Firebase
import com.google.firebase.auth.auth
import com.google.firebase.firestore.firestore
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CreateClassScreen(
onClassCreated: () -> Unit,
onNavigateBack: () -> Unit
) {
var className by remember { mutableStateOf("") }
var subject by remember { mutableStateOf("") }
var isLoading by remember { mutableStateOf(false) }
var errorMessage by remember { mutableStateOf<String?>(null) }
Scaffold(
topBar = {
TopAppBar(
title = { Text("Create a Class") },
navigationIcon = {
IconButton(onClick = onNavigateBack) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "Back"
)
}
}
)
}
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.padding(24.dp)
) {
OutlinedTextField(
value = className,
onValueChange = { className = it },
label = { Text("Class Name") },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(16.dp))
OutlinedTextField(
value = subject,
onValueChange = { subject = it },
label = { Text("Subject") },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(24.dp))
if (errorMessage != null) {
Text(
text = errorMessage!!,
color = MaterialTheme.colorScheme.error,
style = MaterialTheme.typography.bodySmall
)
Spacer(modifier = Modifier.height(8.dp))
}
Button(
onClick = {
if (className.isBlank() || subject.isBlank()) {
errorMessage = "Please fill in all fields"
return@Button
}
isLoading = true
errorMessage = null
val currentUser = Firebase.auth.currentUser ?: return@Button
val classCode = generateClassCode()
val classData = hashMapOf(
"name" to className,
"subject" to subject,
"code" to classCode,
"teacherUid" to currentUser.uid,
"teacherName" to (currentUser.displayName ?: "Teacher"),
"members" to listOf(currentUser.uid)
)
Firebase.firestore.collection("classes")
.add(classData)
.addOnSuccessListener {
isLoading = false
onClassCreated()
}
.addOnFailureListener { e ->
isLoading = false
errorMessage = e.message ?: "Failed to create class"
}
},
modifier = Modifier.fillMaxWidth(),
enabled = !isLoading
) {
if (isLoading) {
CircularProgressIndicator(modifier = Modifier.size(20.dp))
} else {
Text("Create Class")
}
}
}
}
}
private fun generateClassCode(): String {
val chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
return (1..6).map { chars.random() }.joinToString("")
}
package com.example.mobilelearningbestbuddy.screens
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.google.firebase.Firebase
import com.google.firebase.auth.auth
import com.google.firebase.firestore.FieldValue
import com.google.firebase.firestore.firestore
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun JoinClassScreen(
onClassJoined: () -> Unit,
onNavigateBack: () -> Unit
) {
var classCode by remember { mutableStateOf("") }
var isLoading by remember { mutableStateOf(false) }
var errorMessage by remember { mutableStateOf<String?>(null) }
Scaffold(
topBar = {
TopAppBar(
title = { Text("Join a Class") },
navigationIcon = {
IconButton(onClick = onNavigateBack) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "Back"
)
}
}
)
}
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.padding(24.dp)
) {
Text(
text = "Enter the class code provided by your teacher",
style = MaterialTheme.typography.bodyLarge
)
Spacer(modifier = Modifier.height(16.dp))
OutlinedTextField(
value = classCode,
onValueChange = { classCode = it.uppercase() },
label = { Text("Class Code") },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(24.dp))
if (errorMessage != null) {
Text(
text = errorMessage!!,
color = MaterialTheme.colorScheme.error,
style = MaterialTheme.typography.bodySmall
)
Spacer(modifier = Modifier.height(8.dp))
}
Button(
onClick = {
if (classCode.isBlank()) {
errorMessage = "Please enter a class code"
return@Button
}
isLoading = true
errorMessage = null
val currentUser = Firebase.auth.currentUser ?: return@Button
Firebase.firestore.collection("classes")
.whereEqualTo("code", classCode)
.get()
.addOnSuccessListener { documents ->
if (documents.isEmpty) {
isLoading = false
errorMessage = "Invalid class code. Please check and try again."
} else {
val classDoc = documents.documents[0]
classDoc.reference.update(
"members", FieldValue.arrayUnion(currentUser.uid)
).addOnSuccessListener {
isLoading = false
onClassJoined()
}.addOnFailureListener { e ->
isLoading = false
errorMessage = e.message ?: "Failed to join class"
}
}
}
.addOnFailureListener { e ->
isLoading = false
errorMessage = e.message ?: "Failed to find class"
}
},
modifier = Modifier.fillMaxWidth(),
enabled = !isLoading
) {
if (isLoading) {
CircularProgressIndicator(modifier = Modifier.size(20.dp))
} else {
Text("Join Class")
}
}
}
}
}
package com.example.mobilelearningbestbuddy.navigation
import androidx.compose.runtime.Composable
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.example.mobilelearningbestbuddy.screens.CreateClassScreen
import com.example.mobilelearningbestbuddy.screens.DashboardScreen
import com.example.mobilelearningbestbuddy.screens.JoinClassScreen
import com.example.mobilelearningbestbuddy.screens.LoginScreen
import com.example.mobilelearningbestbuddy.screens.RegisterScreen
import com.google.firebase.Firebase
import com.google.firebase.auth.auth
@Composable
fun AppNavigation() {
val navController = rememberNavController()
val startDestination = if (Firebase.auth.currentUser != null) "dashboard" else "login"
NavHost(navController = navController, startDestination = startDestination) {
composable("login") {
LoginScreen(
onNavigateToRegister = { navController.navigate("register") },
onLoginSuccess = {
navController.navigate("dashboard") {
popUpTo("login") { inclusive = true }
}
}
)
}
composable("register") {
RegisterScreen(
onNavigateToLogin = { navController.popBackStack() },
onRegisterSuccess = {
navController.navigate("dashboard") {
popUpTo("login") { inclusive = true }
}
}
)
}
composable("dashboard") {
DashboardScreen(
onNavigateToCreateClass = { navController.navigate("createClass") },
onNavigateToJoinClass = { navController.navigate("joinClass") },
onLogout = {
Firebase.auth.signOut()
navController.navigate("login") {
popUpTo("dashboard") { inclusive = true }
}
}
)
}
composable("createClass") {
CreateClassScreen(
onClassCreated = { navController.popBackStack() },
onNavigateBack = { navController.popBackStack() }
)
}
composable("joinClass") {
JoinClassScreen(
onClassJoined = { navController.popBackStack() },
onNavigateBack = { navController.popBackStack() }
)
}
}
}
package com.example.mobilelearningbestbuddy
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.ui.Modifier
import com.example.mobilelearningbestbuddy.ui.theme.MobileLearningBestBuddyTheme
import com.example.mobilelearningbestbuddy.navigation.AppNavigation
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState){
setContent {
MobileLearningBestBuddyTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
AppNavigation()
}
}
}
}
}
Your app now has full navigation between five screens. Users can register, log in, create classes, and join classes. Next up, you'll take on the Secret Mission and build a screen that displays all enrolled classes.
Secret mission
Build a My Classes Screen
Your app lets users create and join classes, but there's no way to see all their enrolled classes in one place. Build a My Classes screen that queries Firestore for every class the current user belongs to, then displays each one with a role badge showing whether they are the Teacher or a Student.
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
This project uses the Firebase Spark plan (free tier) and runs locally on your machine, so there are no ongoing costs.
Resources you used:
- Firebase project (includes Authentication users and Firestore database).
- Local Android Studio project files.
Keep everything running
No action needed. Choose this if you are still actively building or want to continue experimenting with the app.
- Your Firebase project remains on the Spark free tier with no charges.
- Your Firestore test mode security rules will expire after 30 days. You would need to update them if you continue development after that period.
- Your local project files stay on your machine ready for you to reopen in Android Studio.
Pause - I'll come back to this later
Shut down running processes to free up memory. Your project files and Firebase data stay intact for when you return.
- Stop the Android Emulator by closing the emulator window or clicking the X button on the emulator toolbar.
- Close Android Studio to free up system memory.
- Your Firebase project remains dormant at no cost 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, clicking the gear icon next to Project Overview, then selecting Project settings.
- Scroll to the bottom of the General tab and click Delete project. Confirm the deletion by checking each acknowledgement box.
- Delete your local project files by running the appropriate command for your operating system.
On macOS or Linux:
cd ~/Desktop
rm -rf MobileLearningBestBuddy
On Windows:
cd ~\Desktop
rmdir /s /q MobileLearningBestBuddy
Nice Work!
Nice Work!
You just built a complete Android app called "Mobile Learning Best Buddy" with Kotlin and Jetpack Compose. Users can register, log in, create classes, and join classes with a shareable code.
You've learned how to:
- Implemented secure registration and login with Firebase Authentication, including loading states and error handling.
- Designed and connected a Cloud Firestore database for class management, storing teacher and student relationships with shareable class codes.
- Created a multi-screen navigation system with Navigation Compose, handling conditional start destinations and back-stack management across 6 screens.
- Secret Mission: Built a "My Classes" screen with role-based badges and Firestore array-contains queries to display all enrolled classes dynamically.
Ready to quiz yourself?