Stage 2 · Games, lists & navigationProject 6

Android Basics Quiz

Create a five-question learning app with accessible answer selection, immediate explanations, a progress indicator, guarded scoring, and a restartable result screen.

Your assignment: Android Basics Quiz

Create a five-question learning app with accessible answer selection, immediate explanations, a progress indicator, guarded scoring, and a restartable result screen.

Beginner · 3–4 hours of building

The download is a complete reference app. Read the brief, predict the result, then implement each step in a separate working copy. Code runs in Android Studio on a device or emulator; the website displays and explains the source.

Before you begin

  • Kotlin data classes, lists, zero-based indices, and functions
  • Compose state, conditional UI, and callbacks from Projects 1–5
  • Basic string resources and Android Studio local test execution

By the end, you can…

  • Describe a quiz question with a data model instead of duplicating screens
  • Build a single-choice group with selectable rows and RadioButton
  • Separate selecting, submitting, reviewing, and advancing into explicit states
  • Prevent missing selections and repeated submissions from changing the score
  • Restore the current question and score after rotation and verify scoring with local tests

Required features

  • Provide five local Android or Kotlin questions with four possible answers each.
  • Display one question at a time, its position, and a progress indicator.
  • Require a valid selection before submission. After submission, lock the selection and show the correct answer with an explanation.
  • Award one point for a correct answer and never award a second point for submitting the same question again.
  • Advance only after review. After question five, display a final score out of five and a Restart quiz button.
  • Reset all progress on restart and preserve progress, selection, submission state, and score during rotation.

Set up the project and run the reference app

  1. Open: Extract the ZIP. In Android Studio choose Open, then select the project folder containing settings.gradle.kts. Open that folder rather than just a Kotlin file.
  2. Prepare: Use Android Studio’s bundled JDK 17 or 21 in Gradle settings. In SDK Manager install Android SDK Platform 36 and any Build Tools requested during sync.
  3. Sync: Allow Gradle to download the pinned dependencies. Initial setup requires internet. The app runs offline and needs no backend, login or API key.
  4. Run: In Device Manager create an emulator with API 23 or newer, or connect a device with USB debugging. Select the app run configuration and press Run.
  5. Observe: Try the normal and boundary scenarios from Test the app. Record what changes after each action. Use Logcat to investigate a crash instead of changing several files at once.
  6. Practice: Copy the extracted folder to a working folder. Keep the build files, manifest, resources and CommonUi.kt. Rebuild the application files following the steps below. Attempt each task before consulting its reference.
PowerShell — in the project folder
.\gradlew.bat :app:assembleDebug :app:testDebugUnitTest
macOS/Linux: chmod +x gradlew, then ./gradlew :app:assembleDebug :app:testDebugUnitTest. Unit tests run without a device; manual UI checks need a device or emulator.

Keep the toolchain consistent

The projects use the existing course’s pinned toolchain: Kotlin 2.2.21, AGP 8.13.2, Gradle 8.13, and Compose BOM 2025.12.01. The original collection uses lifecycle 2.9.4. Projects 8 and 10 add Navigation 2.9.6; project 10 adds Room 2.8.4. All build files and the Gradle wrapper are included. Open TOOLCHAIN.md for setup details.

Understand the topics before coding

For each topic, connect the Kotlin concept to a visible behavior. Identify the value the screen reads and the event that can change that value.

TOPIC 01

Data-driven screens

QuizQuestion contains a stable ID, prompt resource, four option resources, a correct index, and an explanation resource. The same screen reads quizQuestions[questionIndex] for every question. Adding content therefore changes the bank rather than introducing another nearly identical composable.

TOPIC 02

A small state machine

The round has three visible stages: answer selection, submitted review, and final results. selectedIndex starts at -1, submitted locks an answered question, and finished selects the result branch. Next resets only the per-question values; Restart resets the whole quiz. Naming these stages makes transitions easier to reason about.

TOPIC 03

Index validation and scoring guards

Options use zero-based indices. canSubmitAnswer checks that the selection refers to an existing option and the question has not been submitted. scoreAfterAnswer adds a point only when that guarded selection matches correctIndex. The same guard controls the button and its event handler, so a repeated event cannot score twice.

TOPIC 04

Single-choice accessibility

The options are a selectableGroup. Each row exposes the RadioButton role and its selected state, allowing the label and radio control to behave as one choice. RadioButton has onClick = null because the row owns the click. Keeping one click target avoids two competing handlers for the same answer.

TOPIC 05

Saved progress and derived UI

The five saved values are enough to reconstruct the current question, selected answer, feedback, and score after rotation. The progress fraction is calculated from questionIndex and submitted rather than saved separately. Recomputing such values avoids a progress bar that disagrees with the current stage.

Three different lifetimes

Recomposition redraws UI from state. rememberSaveable restores small supported values after Activity recreation; a ViewModel retains screen state during configuration changes. Neither replaces permanent storage. The notes project uses Room for records that must remain after closing and reopening the app.

Build the app step by step

Work in this order. The code panels are focused excerpts from the complete files below. They may begin or end inside a function: use the full source browser when copying a file, including its imports. After every step, run the app and check the stated result.

Step 1. Launch the project and identify its files

Your task: Extract the download and open its settings.gradle.kts folder in Android Studio. Sync Gradle and run app on an API 23 or newer emulator. Find MainActivity.kt, QuizQuestions.kt, QuizRules.kt, and res/values/strings.xml. Trace how the activity enters QuizScreen through the shared MiniTheme and MiniScreen helpers.

Open: app/src/main/java/com/mastercoding/miniprojects/quiz/MainActivity.kt

How it works: The activity owns Android hosting, the question file owns local content, the rules file owns scoring policy, and QuizScreen connects events to UI state. This structure lets you improve content without accidentally changing how points are awarded.

MainActivity.kt
import androidx.compose.ui.unit.dp

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MiniTheme {
                MiniScreen(stringResource(R.string.screen_title)) {
                    QuizScreen()
                }
            }
        }
    }
}

@Composable
private fun QuizScreen() {
    var questionIndex by rememberSaveable { mutableIntStateOf(0) }
    var selectedIndex by rememberSaveable { mutableIntStateOf(-1) }
    var submitted by rememberSaveable { mutableStateOf(false) }
    var score by rememberSaveable { mutableIntStateOf(0) }
    var finished by rememberSaveable { mutableStateOf(false) }

    if (finished) {
        Text(stringResource(R.string.completed), style = MaterialTheme.typography.headlineSmall)
        Text(stringResource(R.string.final_score, score, quizQuestions.size))
Reference excerpt · lines 31–56. The full file, with imports, is in Complete source files.

Stop and check

The first question appears with four choices. Submit answer is disabled until a choice is selected.

Step 2. Model a question and construct the local question bank

Your task: Open QuizQuestions.kt. For each question, map the prompt and options to strings.xml and identify correctIndex. Remember that index 0 means the first option. The provided one-based answer positions are 2, 1, 3, 4, and 2. Change the wording of one question without changing its correct answer, then run again.

Open: app/src/main/java/com/mastercoding/miniprojects/quiz/QuizQuestions.kt

How it works: A question is data, not a separate screen. Resource IDs connect each content field to localizable text. IDs identify questions independently of their position; correctIndex identifies an option within one question. When editing the bank, always keep the option order and answer index consistent.

QuizQuestions.kt
package com.mastercoding.miniprojects.quiz

data class QuizQuestion(
    val id: Int,
    val prompt: Int,
    val options: List<Int>,
    val correctIndex: Int,
    val explanation: Int
)

// Resource IDs keep the question bank localizable without mixing UI into scoring.
val quizQuestions = listOf(
    QuizQuestion(1, R.string.question_1,
        listOf(R.string.q1_a, R.string.q1_b, R.string.q1_c, R.string.q1_d),
        correctIndex = 1, explanation = R.string.explanation_1),
    QuizQuestion(2, R.string.question_2,
        listOf(R.string.q2_a, R.string.q2_b, R.string.q2_c, R.string.q2_d),
        correctIndex = 0, explanation = R.string.explanation_2),
    QuizQuestion(3, R.string.question_3,
        listOf(R.string.q3_a, R.string.q3_b, R.string.q3_c, R.string.q3_d),
        correctIndex = 2, explanation = R.string.explanation_3),
    QuizQuestion(4, R.string.question_4,
        listOf(R.string.q4_a, R.string.q4_b, R.string.q4_c, R.string.q4_d),
        correctIndex = 3, explanation = R.string.explanation_4),
    QuizQuestion(5, R.string.question_5,
        listOf(R.string.q5_a, R.string.q5_b, R.string.q5_c, R.string.q5_d),
Reference excerpt · lines 1–26. The full file, with imports, is in Complete source files.

Stop and check

The screen renders your edited wording and still accepts the intended answer. Every question has exactly four choices and one valid correctIndex.

Step 3. Define selection and scoring rules before UI wiring

Your task: Read canSubmitAnswer and scoreAfterAnswer. Trace a missing answer (-1), an invalid option (4 for four options), a correct answer, an incorrect answer, and an already-submitted answer. Write your expected new score for each trace before running the tests. Keep the helper independent of Composable functions and Android resources.

Open: app/src/main/java/com/mastercoding/miniprojects/quiz/QuizRules.kt

How it works: Submission eligibility and correctness are different questions. A correct index must not add a point when that answer was already submitted. Guarding the scoring function protects the score even if a future UI change accidentally calls it twice.

QuizRules.kt

/** An answer can be submitted only once and must refer to an existing option. */
fun canSubmitAnswer(selectedIndex: Int, optionCount: Int, submitted: Boolean): Boolean =
    !submitted && selectedIndex in 0 until optionCount

fun scoreAfterAnswer(
    currentScore: Int,
    selectedIndex: Int,
    correctIndex: Int,
    optionCount: Int,
    submitted: Boolean
): Int {
    require(currentScore >= 0)
    require(correctIndex in 0 until optionCount)
    return currentScore + if (
        canSubmitAnswer(selectedIndex, optionCount, submitted) && selectedIndex == correctIndex
    ) 1 else 0
}
Reference excerpt · lines 2–20. The full file, with imports, is in Complete source files.

Stop and check

Only an unsubmitted, in-range, correct selection increases the score by one. Missing, incorrect, invalid, and repeated answers leave it unchanged.

Step 4. Declare the saved quiz state and render results conditionally

Your task: Inspect questionIndex, selectedIndex, submitted, score, and finished in QuizScreen. Explain why selectedIndex uses -1 instead of defaulting to the first answer. Trace the finished branch and its early return. Answer one question, rotate the emulator, and verify that the same review state and score remain.

Open: app/src/main/java/com/mastercoding/miniprojects/quiz/MainActivity.kt

How it works: A default selected answer would allow a submission the student never chose. The explicit finished branch prevents indexing beyond the question bank when the quiz ends. Saving only the small values needed to reconstruct the UI keeps the result and current question consistent after activity recreation.

MainActivity.kt
@Composable
private fun QuizScreen() {
    var questionIndex by rememberSaveable { mutableIntStateOf(0) }
    var selectedIndex by rememberSaveable { mutableIntStateOf(-1) }
    var submitted by rememberSaveable { mutableStateOf(false) }
    var score by rememberSaveable { mutableIntStateOf(0) }
    var finished by rememberSaveable { mutableStateOf(false) }

    if (finished) {
        Text(stringResource(R.string.completed), style = MaterialTheme.typography.headlineSmall)
        Text(stringResource(R.string.final_score, score, quizQuestions.size))
        Text(stringResource(R.string.review_tip))
        Button(
            onClick = {
                questionIndex = 0
                selectedIndex = -1
                submitted = false
                score = 0
                finished = false
            },
            modifier = Modifier.fillMaxWidth()
        ) { Text(stringResource(R.string.restart)) }
        return
    }

    val question = quizQuestions[questionIndex]
Reference excerpt · lines 46–71. The full file, with imports, is in Complete source files.

Stop and check

Rotation preserves a selection before submission and preserves locked feedback after submission. Completing the final question shows results without an index-out-of-bounds error.

Step 5. Build a radio group with one selection owner

Your task: Find the options Column and its selectableGroup modifier. For each row, connect selected to selectedIndex == index, update selectedIndex in the row click, and set enabled to !submitted. Keep RadioButton onClick null. Test tapping both the text and the radio area; then inspect the group with TalkBack if available.

Open: app/src/main/java/com/mastercoding/miniprojects/quiz/MainActivity.kt

How it works: One selectedIndex guarantees a single answer. The row owns selection so the full labeled area is usable, while the radio indicator reflects that state. The group and RadioButton semantics expose the choice relationship to accessibility services.

MainActivity.kt
    )
    Column(
        modifier = Modifier.selectableGroup(),
        verticalArrangement = Arrangement.spacedBy(4.dp)
    ) {
        question.options.forEachIndexed { index, option ->
            Row(
                modifier = Modifier.fillMaxWidth()
                    .selectable(
                        selected = selectedIndex == index,
                        enabled = !submitted,
                        role = Role.RadioButton,
                        onClick = { selectedIndex = index }
                    )
                    .padding(vertical = 8.dp, horizontal = 4.dp),
                verticalAlignment = Alignment.CenterVertically
            ) {
                RadioButton(
                    selected = selectedIndex == index,
                    onClick = null,
                    enabled = !submitted
                )
                Text(stringResource(option), modifier = Modifier.padding(start = 12.dp))
            }
        }
    }
Reference excerpt · lines 80–105. The full file, with imports, is in Complete source files.

Stop and check

Exactly one answer is selected at a time. The label is clickable, Submit becomes enabled, and all choices lock after submission.

Step 6. Submit once and explain the answer

Your task: Read the Submit button event. Keep the canSubmitAnswer guard around the score update, then set submitted true. Follow the conditional review block that displays Correct or a learning message, the correct answer, and its explanation. Try a wrong answer deliberately and describe what changes and what stays fixed.

Open: app/src/main/java/com/mastercoding/miniprojects/quiz/MainActivity.kt

How it works: Selecting an option does not score it; submitting commits the decision. Feedback is derived from the selected and correct indices, so it agrees with scoring. The polite live region announces the new explanation while Next remains a separate deliberate action.

MainActivity.kt
        onClick = {
            if (canSubmitAnswer(selectedIndex, question.options.size, submitted)) {
                score = scoreAfterAnswer(
                    score, selectedIndex, question.correctIndex, question.options.size, submitted
                )
                submitted = true
            }
        },
        enabled = canSubmitAnswer(selectedIndex, question.options.size, submitted),
        modifier = Modifier.fillMaxWidth()
    ) { Text(stringResource(R.string.submit_answer)) }

    if (submitted) {
        Column(
            modifier = Modifier.semantics { liveRegion = LiveRegionMode.Polite },
            verticalArrangement = Arrangement.spacedBy(8.dp)
        ) {
            Text(stringResource(
                if (selectedIndex == question.correctIndex) R.string.correct else R.string.incorrect
            ))
            Text(stringResource(R.string.correct_answer, stringResource(question.options[question.correctIndex])))
            Text(stringResource(question.explanation))
        }
        Button(
            onClick = {
                if (questionIndex == quizQuestions.lastIndex) {
Reference excerpt · lines 107–132. The full file, with imports, is in Complete source files.

Stop and check

A correct answer adds exactly one point. An incorrect answer shows the correct choice and explanation with no point. Submit is disabled during review.

Step 7. Advance, finish, and restart with explicit resets

Your task: Inspect the Next button. On ordinary questions, increase questionIndex and reset selectedIndex and submitted; on the last question, set finished. Inspect Restart and verify that all five saved values return to their initial values. Finish once with all correct answers and once with all incorrect answers.

Open: app/src/main/java/com/mastercoding/miniprojects/quiz/MainActivity.kt

How it works: Per-question reset must preserve the total score, while full restart must clear it. The final-question check happens before increasing the index. The result screen therefore stays within the list boundary and the next run begins with a truly empty selection.

MainActivity.kt
        Button(
            onClick = {
                if (questionIndex == quizQuestions.lastIndex) {
                    finished = true
                } else {
                    questionIndex += 1
                    selectedIndex = -1
                    submitted = false
                }
            },
            modifier = Modifier.fillMaxWidth()
        ) {
            Text(stringResource(
                if (questionIndex == quizQuestions.lastIndex) R.string.show_results else R.string.next_question
            ))
        }
    }
}
Reference excerpt · lines 130–148. The full file, with imports, is in Complete source files.

Stop and check

The two runs end at 5 / 5 and 0 / 5. Restart returns to question one, score zero, no selection, and disabled Submit.

Step 8. Run tests and check a complete learning flow

Your task: Run QuizRulesTest from its gutter icon or execute gradlew.bat :app:testDebugUnitTest. Then complete the manual checks below, including rotation before and after submitting. In your README, explain the difference between choosing an option, committing an answer, advancing, and restarting.

Open: app/src/test/java/com/mastercoding/miniprojects/quiz/QuizRulesTest.kt

How it works: Local tests verify scoring boundaries, but they cannot prove that answer labels, accessibility semantics, and the review flow are connected correctly. Combining deterministic rules tests with a short device walkthrough covers both responsibilities.

QuizRulesTest.kt
    }

    @Test fun correctAnswersAddOnePointOnlyBeforeSubmission() {
        assertEquals(3, scoreAfterAnswer(2, 1, 1, 4, false))
        assertEquals(3, scoreAfterAnswer(3, 1, 1, 4, true))
    }

    @Test fun incorrectAndMissingAnswersDoNotAddPoints() {
        assertEquals(2, scoreAfterAnswer(2, 0, 1, 4, false))
        assertEquals(2, scoreAfterAnswer(2, -1, 1, 4, false))
    }
}
Reference excerpt · lines 13–25. The full file, with imports, is in Complete source files.

Stop and check

All rule tests pass and a full five-question run produces a score consistent with the answers actually submitted.

Complete source files

Every text file in the ZIP is available here, including Kotlin, resources, Gradle scripts and the student README. The binary Gradle wrapper JAR is included in the ZIP. Source excerpts and downloads are generated from these same files.

Start hereResponsibility
MainActivity.ktThe launcher Activity installs the Compose UI with setContent.
CommonUi.ktMaterial 3 theme and reusable screen container with scrolling and system insets.
Application Kotlin filesState, rules, navigation or persistence introduced in the guide.
app/src/main/resImages, text resources, and the launch theme.
app/build.gradle.ktsSDK settings and the exact libraries used by this app.
README.md / TOOLCHAIN.mdStudent instructions and setup, usable without the website.

Android launcher Activity, setContent entry point and screen composition. Start tracing the app here.

21 complete text files. Copy the selected file with the code panel button, or download the full project above.

MainActivity.kt
package com.mastercoding.miniprojects.quiz

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.selection.selectableGroup
import androidx.compose.material3.Button
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.LiveRegionMode
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.liveRegion
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MiniTheme {
                MiniScreen(stringResource(R.string.screen_title)) {
                    QuizScreen()
                }
            }
        }
    }
}

@Composable
private fun QuizScreen() {
    var questionIndex by rememberSaveable { mutableIntStateOf(0) }
    var selectedIndex by rememberSaveable { mutableIntStateOf(-1) }
    var submitted by rememberSaveable { mutableStateOf(false) }
    var score by rememberSaveable { mutableIntStateOf(0) }
    var finished by rememberSaveable { mutableStateOf(false) }

    if (finished) {
        Text(stringResource(R.string.completed), style = MaterialTheme.typography.headlineSmall)
        Text(stringResource(R.string.final_score, score, quizQuestions.size))
        Text(stringResource(R.string.review_tip))
        Button(
            onClick = {
                questionIndex = 0
                selectedIndex = -1
                submitted = false
                score = 0
                finished = false
            },
            modifier = Modifier.fillMaxWidth()
        ) { Text(stringResource(R.string.restart)) }
        return
    }

    val question = quizQuestions[questionIndex]
    Text(stringResource(R.string.progress, questionIndex + 1, quizQuestions.size))
    LinearProgressIndicator(
        progress = { (questionIndex + if (submitted) 1f else 0f) / quizQuestions.size },
        modifier = Modifier.fillMaxWidth()
    )
    Text(
        stringResource(question.prompt),
        style = MaterialTheme.typography.titleLarge
    )
    Column(
        modifier = Modifier.selectableGroup(),
        verticalArrangement = Arrangement.spacedBy(4.dp)
    ) {
        question.options.forEachIndexed { index, option ->
            Row(
                modifier = Modifier.fillMaxWidth()
                    .selectable(
                        selected = selectedIndex == index,
                        enabled = !submitted,
                        role = Role.RadioButton,
                        onClick = { selectedIndex = index }
                    )
                    .padding(vertical = 8.dp, horizontal = 4.dp),
                verticalAlignment = Alignment.CenterVertically
            ) {
                RadioButton(
                    selected = selectedIndex == index,
                    onClick = null,
                    enabled = !submitted
                )
                Text(stringResource(option), modifier = Modifier.padding(start = 12.dp))
            }
        }
    }
    Button(
        onClick = {
            if (canSubmitAnswer(selectedIndex, question.options.size, submitted)) {
                score = scoreAfterAnswer(
                    score, selectedIndex, question.correctIndex, question.options.size, submitted
                )
                submitted = true
            }
        },
        enabled = canSubmitAnswer(selectedIndex, question.options.size, submitted),
        modifier = Modifier.fillMaxWidth()
    ) { Text(stringResource(R.string.submit_answer)) }

    if (submitted) {
        Column(
            modifier = Modifier.semantics { liveRegion = LiveRegionMode.Polite },
            verticalArrangement = Arrangement.spacedBy(8.dp)
        ) {
            Text(stringResource(
                if (selectedIndex == question.correctIndex) R.string.correct else R.string.incorrect
            ))
            Text(stringResource(R.string.correct_answer, stringResource(question.options[question.correctIndex])))
            Text(stringResource(question.explanation))
        }
        Button(
            onClick = {
                if (questionIndex == quizQuestions.lastIndex) {
                    finished = true
                } else {
                    questionIndex += 1
                    selectedIndex = -1
                    submitted = false
                }
            },
            modifier = Modifier.fillMaxWidth()
        ) {
            Text(stringResource(
                if (questionIndex == quizQuestions.lastIndex) R.string.show_results else R.string.next_question
            ))
        }
    }
}

Test the app with concrete scenarios

Run these scenarios on your device or emulator. Write down the actual result beside the expected result. A successful Gradle build checks compilation; it does not demonstrate tapping, accessibility, keyboard behavior or rotation.

Do thisExpected result
Open a fresh quiz without choosing an option.No option is selected and Submit answer is disabled.
Tap an answer label, then choose a different one.The selection moves so exactly one option is selected.
Submit a correct answer and attempt to submit again.The explanation appears, choices and Submit lock, and only one point is awarded.
Submit an incorrect answer and tap Next.The wrong answer adds no point. The next question starts with no selection or previous feedback.
Rotate before submitting, and rotate again while viewing submitted feedback.The current question, selection, submission lock, and accumulated score remain consistent.
Answer using positions 2, 1, 3, 4, and 2, then show results.The result is 5 / 5 and no sixth question is accessed.
Restart from results and run QuizRulesTest.The UI starts clean at question one and all guard and scoring tests pass.

Your completion checklist

Project acceptance checklist0 / 6 verified

Check an item only after demonstrating it. Progress is saved in this browser.

Troubleshooting: understand the cause

A question grants points more than once.

Cause: Scoring ignores submitted or the handler always increases the score.

Repair: Use canSubmitAnswer in both UI eligibility and the event, then set submitted true after the one score update.

The next question already has an answer selected.

Cause: The previous selectedIndex was not reset.

Repair: Set selectedIndex to -1 and submitted to false when advancing.

The app crashes after question five.

Cause: questionIndex is incremented beyond the list before showing results.

Repair: Check lastIndex first and switch to finished instead of reading another question.

Choosing the visually correct option earns no point.

Cause: The bank uses a one-based answer position as a zero-based correctIndex.

Repair: Use 0 for the first option and verify every edited question against its option order.

Android resource compilation reports an invalid reference for @Composable.

Cause: A string value beginning with @ is interpreted as a resource reference.

Repair: Keep the leading @ escaped as \@ in strings.xml, as shown in the provided question resources.

Submit your app and explain your decisions

  1. Submit the Android Studio project without build caches, .gradle or machine-specific local.properties.
  2. Include two screenshots demonstrating normal behavior and an edge case.
  3. Add a short README recording the device/API version, actions, observed outcomes, and any known limitation.
  4. Answer the questions below using function and state names from your own implementation.
  • Why does the app need both selectedIndex and submitted instead of scoring every time an option is tapped?
  • Which values reset on Next, which reset on Restart, and why are those two transitions different?
  • If you shuffle the options, what must happen to correctIndex so scoring remains correct?
CriterionPointsWhat to demonstrate
Core behavior40All required actions give the expected result.
Validation and lifecycle20Invalid inputs, empty states, rotation and the project's persistence rules work.
Interface and accessibility20Readable contrast, labeled inputs, meaningful image descriptions, scrollable content and usable touch targets.
Code and explanation20Small named functions, clear state ownership and a correct explanation of one event-to-result trace.

Extra challenges after the core works

  • Add a review screen that stores each committed answer and explains the complete result.
  • Shuffle question order once at quiz start. Save the chosen question IDs so rotation does not change the order.
  • Add topic categories and calculate a separate score per topic without duplicating the question screen.

Choose one extension, write its acceptance check first, then implement it without breaking the original scenarios.

Further reading