← Back to Prcatice more · Phase 1
Your assignment: Quote Switcher
Show one of four short learning reminders and switch between them with Previous, Next, and Reset buttons. Practice a Kotlin list, an index, and saved screen state.
Phase 1 · Simple Compose practice
Beginner · 0.5–1 hours of building
Before you begin
- Create and run an Empty Activity project in Android Studio.
- Read basic Kotlin functions, strings, and if expressions.
- Complete the Phase 1 lessons on composables, layouts, modifiers, and basic state.
By the end, you can…
- Read a string from a fixed Kotlin list using an index.
- Move backward and forward while keeping the index valid.
- Display a human-readable position derived from a zero-based index.
- Restore the current reminder after rotation.
Required features
- Keep four original practice messages bundled in the source.
- Display one reminder and its position from 1 to 4.
- Wrap Next from the fourth reminder to the first.
- Wrap Previous from the first reminder to the fourth.
- Reset to the first reminder and restore the current index after rotation.
Set up the project and run the reference app
- 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. - 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.
- Sync: Allow Gradle to download the pinned dependencies. Initial setup requires internet. The app runs offline and needs no backend, login or API key.
- Run: In Device Manager create an emulator with API 23 or newer, or connect a device with USB debugging. Select the
apprun configuration and press Run. - 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.
- 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.
.\gradlew.bat :app:assembleDebug :app:testDebugUnitTestKeep the toolchain consistent
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.
Data and position
quotes contains the four fixed messages. quoteIndex identifies which message is visible. The app saves only the integer position; the list stays ordinary fixed data and does not need mutable state.
Zero-based indexes
The first list element has index 0, but the screen says reminder 1 of 4. Adding one for display makes the count natural for the user without changing the index used to read the list.
Wrapping safely
Next adds one and takes the remainder after dividing by the list size. Previous adds the list size before subtracting one, then takes the remainder. Starting at 0 with four messages, Previous becomes (0 + 4 - 1) % 4 = 3 rather than a negative index.
State and screen updates
rememberSaveable can restore small values after Activity recreation, such as rotation. It does not permanently save your choices. Static content needs no mutable state.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. Run the quote screen host
Your task: Open MainActivity and follow the supplied MiniTheme and MiniScreen calls into QuoteSwitcher.
Open: app/src/main/java/com/mastercoding/miniprojects/quotes/MainActivity.kt
How it works: All interaction happens on one screen. The activity only hosts the composable, so changing the reminder does not involve navigation or creating a new activity.
@Composable
fun QuoteSwitcher() {
val quotes = listOf(
"Small steps make a working app.",
"Read the result, then improve one thing.",
"A clear name makes code easier to explain.",
"Practice grows with every finished screen."
)
var quoteIndex by rememberSaveable { mutableStateOf(0) }
Text("Learning reminder ${quoteIndex + 1} of ${quotes.size}")
Card(modifier = Modifier.fillMaxWidth()) {
Column(
modifier = Modifier.padding(24.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Text(quotes[quoteIndex], style = MaterialTheme.typography.headlineSmall)
Text("Original practice messages", style = MaterialTheme.typography.labelLarge)
}
}
Button(
onClick = { quoteIndex = (quoteIndex + quotes.size - 1) % quotes.size },
modifier = Modifier.fillMaxWidth()
) { Text("Previous reminder") }
Button(Stop and check
Step 2. Define the fixed messages
Your task: Create quotes with the four supplied original strings using listOf.
Open: app/src/main/java/com/mastercoding/miniprojects/quotes/MainActivity.kt
How it works: The list contents do not change during this exercise. Keeping them as ordinary data makes it clear that the selected position, rather than the messages themselves, is the changing state.
@Composable
fun QuoteSwitcher() {
val quotes = listOf(
"Small steps make a working app.",
"Read the result, then improve one thing.",
"A clear name makes code easier to explain.",
"Practice grows with every finished screen."
)
var quoteIndex by rememberSaveable { mutableStateOf(0) }
Text("Learning reminder ${quoteIndex + 1} of ${quotes.size}")
Card(modifier = Modifier.fillMaxWidth()) {
Column(
modifier = Modifier.padding(24.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Text(quotes[quoteIndex], style = MaterialTheme.typography.headlineSmall)
Text("Original practice messages", style = MaterialTheme.typography.labelLarge)
}
}
Button(
onClick = { quoteIndex = (quoteIndex + quotes.size - 1) % quotes.size },
modifier = Modifier.fillMaxWidth()
) { Text("Previous reminder") }
Button(
onClick = { quoteIndex = (quoteIndex + 1) % quotes.size },Stop and check
Step 3. Save the current position
Your task: Declare quoteIndex with rememberSaveable and initial value 0. Use quoteIndex + 1 for the position label and quotes[quoteIndex] for the card text.
Open: app/src/main/java/com/mastercoding/miniprojects/quotes/MainActivity.kt
How it works: The index is zero-based for Kotlin, but the user sees a one-based count. Both outputs derive from the same integer, preventing a mismatch between the number and the displayed message.
"Practice grows with every finished screen."
)
var quoteIndex by rememberSaveable { mutableStateOf(0) }
Text("Learning reminder ${quoteIndex + 1} of ${quotes.size}")
Card(modifier = Modifier.fillMaxWidth()) {
Column(
modifier = Modifier.padding(24.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Text(quotes[quoteIndex], style = MaterialTheme.typography.headlineSmall)
Text("Original practice messages", style = MaterialTheme.typography.labelLarge)
}
}
Button(
onClick = { quoteIndex = (quoteIndex + quotes.size - 1) % quotes.size },
modifier = Modifier.fillMaxWidth()
) { Text("Previous reminder") }
Button(
onClick = { quoteIndex = (quoteIndex + 1) % quotes.size },
modifier = Modifier.fillMaxWidth()
) { Text("Next reminder") }
Button(onClick = { quoteIndex = 0 }, modifier = Modifier.fillMaxWidth()) {
Text("Back to first reminder")
}
Text("The last reminder wraps to the first. Your current position survives rotation.")Stop and check
Step 4. Add forward and backward wrapping
Your task: Set Next to (quoteIndex + 1) % quotes.size. Set Previous to (quoteIndex + quotes.size - 1) % quotes.size.
Open: app/src/main/java/com/mastercoding/miniprojects/quotes/MainActivity.kt
How it works: The remainder operation maps each step back into 0 through 3. Adding the list size before moving backward avoids the negative remainder that a simple index-minus-one expression can produce.
}
Button(
onClick = { quoteIndex = (quoteIndex + quotes.size - 1) % quotes.size },
modifier = Modifier.fillMaxWidth()
) { Text("Previous reminder") }
Button(
onClick = { quoteIndex = (quoteIndex + 1) % quotes.size },
modifier = Modifier.fillMaxWidth()
) { Text("Next reminder") }
Button(onClick = { quoteIndex = 0 }, modifier = Modifier.fillMaxWidth()) {
Text("Back to first reminder")
}
Text("The last reminder wraps to the first. Your current position survives rotation.")
}
@Preview(showBackground = true)
@Composable
private fun QuoteSwitcherPreview() {
MiniTheme { MiniScreen("Quote Switcher") { QuoteSwitcher() } }
}Stop and check
Step 5. Reset to the first message
Your task: Add Back to first reminder and assign 0 in its callback.
Open: app/src/main/java/com/mastercoding/miniprojects/quotes/MainActivity.kt
How it works: The reset uses the same state owner as the navigation buttons. The position label and message both return to their initial values as a result of that single assignment.
modifier = Modifier.fillMaxWidth()
) { Text("Next reminder") }
Button(onClick = { quoteIndex = 0 }, modifier = Modifier.fillMaxWidth()) {
Text("Back to first reminder")
}
Text("The last reminder wraps to the first. Your current position survives rotation.")
}
@Preview(showBackground = true)
@Composable
private fun QuoteSwitcherPreview() {
MiniTheme { MiniScreen("Quote Switcher") { QuoteSwitcher() } }
}Stop and check
Step 6. Preview and check restoration
Your task: Open QuoteSwitcherPreview. On a device, choose the third reminder, rotate, and then continue using Next and Previous.
Open: app/src/main/java/com/mastercoding/miniprojects/quotes/MainActivity.kt
How it works: A preview verifies appearance, while rotation verifies that rememberSaveable restores the integer before the recreated screen reads the current message.
@Preview(showBackground = true)
@Composable
private fun QuoteSwitcherPreview() {
MiniTheme { MiniScreen("Quote Switcher") { QuoteSwitcher() } }
}Stop and check
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 here | Responsibility |
|---|---|
| MainActivity.kt | The launcher Activity installs the Compose UI with setContent. |
| CommonUi.kt | Material 3 theme and reusable screen container with scrolling and system insets. |
| Application Kotlin files | Layouts, button callbacks and simple saved state introduced in the guide. |
| app/src/main/res | Images, text resources, and the launch theme. |
| app/build.gradle.kts | SDK settings and the exact libraries used by this app. |
| README.md / TOOLCHAIN.md | Student instructions and setup, usable without the website. |
Android launcher Activity, setContent entry point and screen composition. Start tracing the app here.
17 complete text files. Copy the selected file with the code panel button, or download the full project above.
package com.mastercoding.miniprojects.quotes
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.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent { MiniTheme { MiniScreen("Quote Switcher") { QuoteSwitcher() } } }
}
}
@Composable
fun QuoteSwitcher() {
val quotes = listOf(
"Small steps make a working app.",
"Read the result, then improve one thing.",
"A clear name makes code easier to explain.",
"Practice grows with every finished screen."
)
var quoteIndex by rememberSaveable { mutableStateOf(0) }
Text("Learning reminder ${quoteIndex + 1} of ${quotes.size}")
Card(modifier = Modifier.fillMaxWidth()) {
Column(
modifier = Modifier.padding(24.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Text(quotes[quoteIndex], style = MaterialTheme.typography.headlineSmall)
Text("Original practice messages", style = MaterialTheme.typography.labelLarge)
}
}
Button(
onClick = { quoteIndex = (quoteIndex + quotes.size - 1) % quotes.size },
modifier = Modifier.fillMaxWidth()
) { Text("Previous reminder") }
Button(
onClick = { quoteIndex = (quoteIndex + 1) % quotes.size },
modifier = Modifier.fillMaxWidth()
) { Text("Next reminder") }
Button(onClick = { quoteIndex = 0 }, modifier = Modifier.fillMaxWidth()) {
Text("Back to first reminder")
}
Text("The last reminder wraps to the first. Your current position survives rotation.")
}
@Preview(showBackground = true)
@Composable
private fun QuoteSwitcherPreview() {
MiniTheme { MiniScreen("Quote Switcher") { QuoteSwitcher() } }
}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 this | Expected result |
|---|---|
| Launch the app. | The card says Small steps make a working app. and the counter reads Learning reminder 1 of 4. |
| Press Next reminder four times. | The app visits reminders 2, 3, and 4, then returns to reminder 1. |
| Press Previous reminder while on the first reminder. | The fourth reminder appears without an index error. |
| Move to reminder 3 and rotate the device. | Reminder 3 and its message remain visible. |
| Press Back to first reminder after moving elsewhere. | The first message and position 1 of 4 return together. |
| Repeat both direction buttons and use a large system font. | The position remains between 1 and 4, messages stay readable, and all controls remain reachable. |
Your completion checklist
Check an item only after demonstrating it. Progress is saved in this browser.
Troubleshooting: understand the cause
Previous crashes on the first reminder.
Cause: The callback subtracts one and reads quotes[-1], or applies remainder to a negative number.
Repair: Use (quoteIndex + quotes.size - 1) % quotes.size before reading the message.
The position label and quote disagree.
Cause: They use separate mutable values.
Repair: Derive both the label and message from the same quoteIndex.
Submit your app and explain your decisions
- Submit the Android Studio project without build caches,
.gradleor machine-specificlocal.properties. - Include two screenshots demonstrating normal behavior and an edge case.
- Add a short README recording the device/API version, actions, observed outcomes, and any known limitation.
- Answer the questions below using function and state names from your own implementation.
- Why does the screen display quoteIndex + 1 while the list uses quoteIndex?
- Calculate Previous when quoteIndex is 0 and there are four messages.
- Which value needs saved state, and why does the fixed list not need it?
| Criterion | Points | What to demonstrate |
|---|---|---|
| Core behavior | 40 | All required actions give the expected result. |
| Validation and lifecycle | 20 | Invalid inputs, empty states, rotation and the project's persistence rules work. |
| Interface and accessibility | 20 | Readable contrast, labeled inputs, meaningful image descriptions, scrollable content and usable touch targets. |
| Code and explanation | 20 | Small named functions, clear state ownership and a correct explanation of one event-to-result trace. |
Extra challenges after the core works
- Add a fifth original practice message and verify wrapping without changing the index formulas.
- Extract the quote card into a composable that receives a message string.
Choose one extension, write its acceptance check first, then implement it without breaking the original scenarios.