What You Will Build
A quiz app with a progress bar showing question position, question cards, multiple-choice options with color-coded feedback (green for correct, red for wrong), a "Next" button that appears after answering, and a final score screen with a "Play Again" button.
Why This Pattern Matters
Quiz and survey apps teach multi-step state machines, conditional UI rendering, and user feedback patterns. The same architecture applies to onboarding flows, forms with validation, and educational apps.
Step 1: Question Model and State
data class Question(
val text: String,
val options: List<String>,
val correct: Int // index of correct option
)
val questions = listOf(
Question("What is the capital of France?",
listOf("London", "Paris", "Berlin", "Madrid"), 1),
Question("Which planet is closest to the Sun?",
listOf("Venus", "Mars", "Mercury", "Earth"), 2),
Question("What is 2 + 2?",
listOf("3", "4", "5", "6"), 1)
)
var current by remember { mutableIntStateOf(0) }
var score by remember { mutableIntStateOf(0) }
var answered by remember { mutableIntStateOf(-1) }
Step 2: Progress and Question Card
LinearProgressIndicator(
progress = { (current + 1f) / questions.size },
modifier = Modifier.fillMaxWidth()
)
Text("Question ${current + 1}/${questions.size}")
if (current < questions.size) {
val q = questions[current]
Card(Modifier.fillMaxWidth()) {
Text(q.text, Modifier.padding(20.dp),
fontSize = 18.sp,
fontWeight = FontWeight.SemiBold)
}
}
Step 3: Answer Options with Color Feedback
q.options.forEachIndexed { idx, opt ->
val bgColor = when {
answered < 0 -> MaterialTheme.colorScheme.surface
idx == q.correct ->
Color(0xFF4CAF50).copy(alpha = 0.2f) // green
idx == answered ->
Color(0xFFF44336).copy(alpha = 0.2f) // red
else -> MaterialTheme.colorScheme.surface
}
Card(
Modifier.fillMaxWidth().padding(vertical = 4.dp)
.clickable(enabled = answered < 0) {
answered = idx
if (idx == q.correct) score++
},
colors = CardDefaults.cardColors(
containerColor = bgColor
)
) {
Text(opt, Modifier.padding(16.dp))
}
}
// Next button appears after answering
if (answered >= 0) {
Button(onClick = { current++; answered = -1 }) {
Text("Next")
}
}
Step 4: Score Screen
// When all questions answered:
Text("Score: $score/${questions.size}",
fontSize = 36.sp, fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary)
Button(onClick = { current = 0; score = 0 }) {
Text("Play Again")
}
Tips and Pitfalls
- answered = -1 means unanswered. This sentinel value gates both the color logic and the clickable enabled state.
- Color feedback uses 20% alpha so the text remains readable. Full-opacity backgrounds would obscure content.
- State reset on "Play Again": Reset
current,score, andanswered. Missing any one causes bugs. - Real app extension: Shuffle questions with
questions.shuffled()and add a countdown timer withLaunchedEffect.