Almost every real app has more than one screen. A shopping app has a list, a product page, a cart and a checkout. In Compose you do not create many Activities for this. You keep one Activity and use Navigation Compose, a library that swaps which composable is shown and keeps a back stack, so the system Back button works as users expect.
Concepts in this project
- NavController: remembers where you are and moves between screens.
- NavHost: the place on screen where the current destination is drawn. It lists all the routes.
- Routes with arguments: a route such as
result/{score}/{total}carries values to the next screen. - rememberSaveable: keeps the quiz position through rotation.
- Back stack: after the result screen you pop back to home so Back does not return to the last question.
Create the project
- New Empty Activity project
QuizApp, packagecom.example.quizapp. - Add the Navigation Compose line to the dependencies and sync.
- Create
Questions.ktandQuizApp.kt, and replaceMainActivity.kt.
`app/build.gradle.kts (dependencies)`
// app/build.gradle.kts -> dependencies { ... }
// A new "Empty Activity" project already has Compose, Material 3 and the activity library.
// Projects 2 to 4 need these extra lines:
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.7") // viewModel() in Compose
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.8.7") // collectAsStateWithLifecycle()
implementation("androidx.navigation:navigation-compose:2.8.5") // project 4 only
implementation("androidx.compose.material:material-icons-core") // Add and Delete icons (version comes from the Compose BOM)
QuizApp/app/src/main/java/com/example/quizapp/
MainActivity.kt starts the app
Questions.kt the quiz data
QuizApp.kt NavHost and the three screens
The code
`app/src/main/java/com/example/quizapp/Questions.kt`
package com.example.quiz
data class Question(val text: String, val options: List<String>, val answerIndex: Int)
val questions = listOf(
Question("Which language is the main language for Android apps today?", listOf("Kotlin", "COBOL", "Swift"), 0),
Question("What do you call a function that draws UI in Jetpack Compose?", listOf("A composable", "A servlet", "A layout file"), 0),
Question("Which keyword makes a value that cannot be reassigned?", listOf("var", "val", "mut"), 1),
Question("What does the safe-call operator ?. do?", listOf("Stops when the value is null instead of crashing", "Deletes a file", "Starts a coroutine"), 0),
Question("Which tool turns your code into an installable Android app file?", listOf("Gradle", "Photoshop", "Excel"), 0),
)
`app/src/main/java/com/example/quizapp/QuizApp.kt`
package com.example.quiz
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument
@Composable
fun QuizApp() {
val nav = rememberNavController()
NavHost(navController = nav, startDestination = "home") {
composable("home") {
HomeScreen(onStart = { nav.navigate("quiz") })
}
composable("quiz") {
QuizScreen(onFinished = { score ->
// Put the score in the route, and drop the quiz screen from the back stack.
nav.navigate("result/$score/${questions.size}") { popUpTo("home") }
})
}
composable(
route = "result/{score}/{total}",
arguments = listOf(
navArgument("score") { type = NavType.IntType },
navArgument("total") { type = NavType.IntType }
)
) { entry ->
val score = entry.arguments?.getInt("score") ?: 0
val total = entry.arguments?.getInt("total") ?: 0
ResultScreen(score, total, onAgain = { nav.navigate("quiz") { popUpTo("home") } })
}
}
}
@Composable
fun HomeScreen(onStart: () -> Unit) {
Column(
Modifier.fillMaxSize().systemBarsPadding().padding(24.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("Android Quiz", style = MaterialTheme.typography.displaySmall)
Spacer(Modifier.height(8.dp))
Text("${questions.size} questions. Pick one answer for each.")
Spacer(Modifier.height(24.dp))
Button(onClick = onStart) { Text("Start") }
}
}
@Composable
fun QuizScreen(onFinished: (Int) -> Unit) {
// rememberSaveable survives screen rotation; plain remember does not.
var index by rememberSaveable { mutableIntStateOf(0) }
var score by rememberSaveable { mutableIntStateOf(0) }
var picked by rememberSaveable { mutableStateOf<Int?>(null) }
val q = questions[index]
Column(Modifier.fillMaxSize().systemBarsPadding().padding(24.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
LinearProgressIndicator(progress = { (index + 1) / questions.size.toFloat() }, modifier = Modifier.fillMaxWidth())
Text("Question ${index + 1} of ${questions.size}")
Text(q.text, style = MaterialTheme.typography.titleLarge)
q.options.forEachIndexed { i, option ->
val colors = when {
picked == null -> ButtonDefaults.outlinedButtonColors()
i == q.answerIndex -> ButtonDefaults.outlinedButtonColors(containerColor = MaterialTheme.colorScheme.primaryContainer)
i == picked -> ButtonDefaults.outlinedButtonColors(containerColor = MaterialTheme.colorScheme.errorContainer)
else -> ButtonDefaults.outlinedButtonColors()
}
OutlinedButton(
onClick = { if (picked == null) { picked = i; if (i == q.answerIndex) score++ } },
colors = colors,
modifier = Modifier.fillMaxWidth()
) { Text(option) }
}
Spacer(Modifier.weight(1f))
Button(
onClick = {
if (index == questions.lastIndex) onFinished(score) else { index++; picked = null }
},
enabled = picked != null,
modifier = Modifier.fillMaxWidth()
) { Text(if (index == questions.lastIndex) "See result" else "Next") }
}
}
@Composable
fun ResultScreen(score: Int, total: Int, onAgain: () -> Unit) {
Column(
Modifier.fillMaxSize().systemBarsPadding().padding(24.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("You scored", style = MaterialTheme.typography.titleMedium)
Text("$score / $total", style = MaterialTheme.typography.displayLarge)
Spacer(Modifier.height(24.dp))
Button(onClick = onAgain) { Text("Try again") }
}
}
`app/src/main/java/com/example/quizapp/MainActivity.kt`
package com.example.quiz
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent { MaterialTheme { Surface { QuizApp() } } }
}
}
How it works
NavHoststarts athome. Pressing Start callsnavigate("quiz").- The quiz screen keeps
index,scoreand the chosen answer inrememberSaveable, so rotating the phone does not restart the quiz. - After the last question it navigates to
result/{score}/{total}and removes the quiz from the back stack withpopUpTo. - The result screen reads the two numbers from the route arguments and offers Play again, which returns to home.
Expected output
The home screen shows the quiz title and a Start button. Each question screen shows Question 1 of N, the question and four answer buttons. After an answer is tapped, the correct one turns green and a wrong choice turns red, and a Next button appears. After the last question the result screen says, for example, "You scored 4 out of 5". Pressing Back on the result screen leaves the app rather than reopening the last question.
Your task
- Build the app and play through once, then rotate the phone mid-quiz.
- Add three more questions to
Questions.kt. - Show a message on the result screen that depends on the score, such as a different line for a perfect score.
- Add a Settings screen reachable from home and test that Back returns to home.
How to check it worked
- Rotation keeps the current question and score.
- Back from the result screen leaves the quiz instead of re-entering it.
- You can explain what a route and a back stack are.