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
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.
- Extract the ZIP. In Android Studio, open the project folder containing
settings.gradle.kts. - Use JDK 17 or 21 and install Android SDK Platform 36 plus the Build Tools requested by Android Studio.
- Allow the first Gradle sync to download dependencies. No backend account or API key is needed.
- Select an emulator or device running API 23 or newer, then run the
appconfiguration. - Start with the expected results below. Open
README.mdandTOOLCHAIN.mdfor the exact project instructions.
.\gradlew.bat :app:assembleDebug :app:testDebugUnitTestKeep the first version small
Implementation roadmap
- Own the two values: The first launch shows Light and a study tip.
- Apply the selected theme: The switch changes the whole surface and text colors.
- Pass values and callbacks: Each control reflects the current value supplied by its parent.
- Show useful content: Turning tips off hides only the tip text.
- 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
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,Expected behavior
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
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,Expected behavior
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
@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" })
}Expected behavior
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
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.")
}
}Expected behavior
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
}
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.")
}
}Expected behavior
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.
| File | Purpose |
|---|---|
| app/src/main/java/com/mastercoding/phase5/MainActivity.kt | Activity 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.
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
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.
| Action | Expected 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
| Criterion | Points | Evidence |
|---|---|---|
| Core behavior | 40 | All acceptance scenarios produce their expected results. |
| Clear state ownership | 30 | Trace one event from its callback to the displayed result and identify the owner of each value. |
| Boundary cases | 20 | Verify the listed invalid, empty, and restoration cases. |
| Readable implementation | 10 | Use 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.