Phase 5 · Screen Behavior & QualityModule 49~30 min read

Phase 5 Capstone · Study Preferences

Build one settings screen with two switches and Reset. Practice state hoisting, conditional content, and a readable light/dark theme. Settings use saved UI state; durable preferences are a separate DataStore exercise.

Phase 5: Study Preferences

Build one settings screen with two switches and Reset. Practice state hoisting, conditional content, and a readable light/dark theme. Settings use saved UI state; durable preferences are a separate DataStore exercise.

Guided workshop: 1–2 hours

Build the required features in order. Each step has an expected result and a complete reference file. All projects work offline after the initial dependency download.

Before you start

  • Boolean state and callbacks
  • MaterialTheme
  • Conditional content

Application structure and responsibilities

  • One screen with a single main task.

Concepts to explain: State hoisting · Boolean state · Conditional content · MaterialTheme · Saved UI state

What you will practice

  • State hoisting
  • Boolean state
  • Conditional content
  • MaterialTheme
  • Saved UI state

Download the complete project and run it

Each ZIP includes the application source, build scripts, Gradle wrapper, manifest, resources and README. The full solution is also readable below; no feature is left as an unfinished TODO.

  1. Extract the ZIP. In Android Studio, open the project folder containing settings.gradle.kts.
  2. Use JDK 17 or 21 and install Android SDK Platform 36 plus the Build Tools requested by Android Studio.
  3. Allow the first Gradle sync to download dependencies. No backend account or API key is needed.
  4. Select an emulator or device running API 23 or newer, then run the app configuration.
  5. Start with the expected results below. Open README.md and TOOLCHAIN.md for the exact project instructions.
PowerShell
.\gradlew.bat :app:assembleDebug :app:testDebugUnitTest
Run inside the extracted project. macOS/Linux: chmod +x gradlew, then ./gradlew :app:assembleDebug :app:testDebugUnitTest. UI tests need a running emulator: :app:connectedDebugAndroidTest.

Keep the first version small

Run the reference app, trace one input through the code, and reproduce its behavior before changing it. The short English labels are sample labels; localization is an optional follow-up exercise.

Implementation roadmap

  1. Own the two values: The first launch shows Light and a study tip.
  2. Apply the selected theme: The switch changes the whole surface and text colors.
  3. Pass values and callbacks: Each control reflects the current value supplied by its parent.
  4. Show useful content: Turning tips off hides only the tip text.
  5. Reset and verify: Reset selects light mode and shows tips. Rotation restores the selected values.

Build the app step by step

Follow these steps in order. Each code window shows the relevant part of a real project file; the complete, unabridged files are in the source browser and ZIP. Run and check each small change before moving on.

Step 1: Own the two values

The Activity content owns dark and showTips. Start in light mode with tips visible. Small Boolean values use rememberSaveable for recreation.

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

MainActivity.kt
        enableEdgeToEdge()
        setContent {
            var dark by rememberSaveable { mutableStateOf(false) }
            var showTips by rememberSaveable { mutableStateOf(true) }
            MaterialTheme(colorScheme = if (dark) darkColorScheme() else lightColorScheme()) {
                Scaffold { padding ->
                    PreferencesScreen(dark, showTips, { dark = it }, { showTips = it },
                        { dark = false; showTips = true }, Modifier.padding(padding))
                }
            }
        }
    }
}

@Composable
fun PreferencesScreen(
    dark: Boolean,
    showTips: Boolean,
    onDarkChange: (Boolean) -> Unit,
Focused excerpt, lines 21–39. Select this file in Complete source code for every line.

Expected behavior

The first launch shows Light and a study tip.

Step 2: Apply the selected theme

Select the Material color scheme from dark. The same screen implementation uses the chosen theme.

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

MainActivity.kt
            var dark by rememberSaveable { mutableStateOf(false) }
            var showTips by rememberSaveable { mutableStateOf(true) }
            MaterialTheme(colorScheme = if (dark) darkColorScheme() else lightColorScheme()) {
                Scaffold { padding ->
                    PreferencesScreen(dark, showTips, { dark = it }, { showTips = it },
                        { dark = false; showTips = true }, Modifier.padding(padding))
                }
            }
        }
    }
}

@Composable
fun PreferencesScreen(
    dark: Boolean,
    showTips: Boolean,
    onDarkChange: (Boolean) -> Unit,
    onTipsChange: (Boolean) -> Unit,
    onReset: () -> Unit,
Focused excerpt, lines 23–41. Select this file in Complete source code for every line.

Expected behavior

The switch changes the whole surface and text colors.

Step 3: Pass values and callbacks

The screen receives values and events. It does not keep copies of the settings.

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

MainActivity.kt

@Composable
fun PreferencesScreen(
    dark: Boolean,
    showTips: Boolean,
    onDarkChange: (Boolean) -> Unit,
    onTipsChange: (Boolean) -> Unit,
    onReset: () -> Unit,
    modifier: Modifier = Modifier,
) {
    Column(modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(24.dp),
        verticalArrangement = Arrangement.spacedBy(16.dp)) {
        Text("Study Preferences", style = MaterialTheme.typography.headlineMedium)
        Text("Appearance: " + if (dark) "Dark" else "Light")
        Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
            Text("Dark appearance", Modifier.weight(1f))
            Switch(checked = dark, onCheckedChange = onDarkChange,
                modifier = Modifier.semantics { contentDescription = "Dark appearance" })
        }
Focused excerpt, lines 34–52. Select this file in Complete source code for every line.

Expected behavior

Each control reflects the current value supplied by its parent.

Step 4: Show useful content

Use a normal conditional to show a tip. A state change updates the content immediately.

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

MainActivity.kt
                modifier = Modifier.semantics { contentDescription = "Show study tips" })
        }
        if (showTips) Text("Practice one small feature, then check its result.")
        Button(onClick = onReset) { Text("Reset preferences") }
        Text("These settings restore after rotation. They are not written to permanent storage.")
    }
}
Focused excerpt, lines 56–63. Select this file in Complete source code for every line.

Expected behavior

Turning tips off hides only the tip text.

Step 5: Reset and verify

Reset restores both defaults together. Rotate after changing a setting, then check large text and TalkBack labels.

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

MainActivity.kt
        }
        if (showTips) Text("Practice one small feature, then check its result.")
        Button(onClick = onReset) { Text("Reset preferences") }
        Text("These settings restore after rotation. They are not written to permanent storage.")
    }
}
Focused excerpt, lines 57–63. Select this file in Complete source code for every line.

Expected behavior

Reset selects light mode and shows tips. Rotation restores the selected values.

Complete source code — every file

Start with MainActivity.kt, then follow the file guide. The selector also contains the tests, manifest, resources and complete Gradle configuration. The binary Gradle wrapper JAR is included in the download.

FilePurpose
app/src/main/java/com/mastercoding/phase5/MainActivity.ktActivity setup, state owner, and a small Compose screen.

Activity setup, state owner, and a small Compose screen.

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

MainActivity.kt
package com.mastercoding.phase5

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.unit.dp

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            var dark by rememberSaveable { mutableStateOf(false) }
            var showTips by rememberSaveable { mutableStateOf(true) }
            MaterialTheme(colorScheme = if (dark) darkColorScheme() else lightColorScheme()) {
                Scaffold { padding ->
                    PreferencesScreen(dark, showTips, { dark = it }, { showTips = it },
                        { dark = false; showTips = true }, Modifier.padding(padding))
                }
            }
        }
    }
}

@Composable
fun PreferencesScreen(
    dark: Boolean,
    showTips: Boolean,
    onDarkChange: (Boolean) -> Unit,
    onTipsChange: (Boolean) -> Unit,
    onReset: () -> Unit,
    modifier: Modifier = Modifier,
) {
    Column(modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(24.dp),
        verticalArrangement = Arrangement.spacedBy(16.dp)) {
        Text("Study Preferences", style = MaterialTheme.typography.headlineMedium)
        Text("Appearance: " + if (dark) "Dark" else "Light")
        Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
            Text("Dark appearance", Modifier.weight(1f))
            Switch(checked = dark, onCheckedChange = onDarkChange,
                modifier = Modifier.semantics { contentDescription = "Dark appearance" })
        }
        Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
            Text("Show study tips", Modifier.weight(1f))
            Switch(checked = showTips, onCheckedChange = onTipsChange,
                modifier = Modifier.semantics { contentDescription = "Show study tips" })
        }
        if (showTips) Text("Practice one small feature, then check its result.")
        Button(onClick = onReset) { Text("Reset preferences") }
        Text("These settings restore after rotation. They are not written to permanent storage.")
    }
}

Definition of done

Project acceptance checklist0 / 5 verified

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

Try the reference solution yourself

Run each action and compare the result. Record the device, action and observed outcome. For the testing phase, run the included unit tests and the Compose test separately: a JVM test does not prove on-device UI behavior.

ActionExpected result
Turn on Dark appearance.The appearance label says Dark and the theme changes.
Turn off Show study tips.The tip disappears and the other setting remains unchanged.
Change both settings, then Reset.Light appearance and visible tips return.
Change a setting and rotate.The selected Boolean values restore.

Common mistakes and repairs

  • Keep one state owner. Pass values and callbacks to reusable UI.
  • Check invalid and boundary values before adding another feature.

Assessment rubric

CriterionPointsEvidence
Core behavior40All acceptance scenarios produce their expected results.
Clear state ownership30Trace one event from its callback to the displayed result and identify the owner of each value.
Boundary cases20Verify the listed invalid, empty, and restoration cases.
Readable implementation10Use descriptive names and small functions; describe any known limitation.

Total: 100 points. Demonstrate the working app, trace one event through the source, and verify a boundary case.

Optional extensions after the core works

  • Move the English labels into string resources after the core checks pass.

After the intervening lessons, continue with Tested Score Calculator.

For deeper study, use the official Compose course and Android architecture guide.