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, and answered. Missing any one causes bugs.
  • Real app extension: Shuffle questions with questions.shuffled() and add a countdown timer with LaunchedEffect.

Get New Tutorials by Email

No spam. Just clear, practical breakdowns you can apply right away.

Enjoy this tutorial?

Get new practical tech tutorials in your inbox.