← Back to Prcatice more · Phase 1
Your assignment: Study Session Planner
Plan a study session with a block counter and a button that includes breaks. Practice Int and Boolean state, bounded callbacks, conditional arithmetic, and a clear total.
Phase 1 · Simple Compose practice
Beginner · 1–2 hours of building
Before you begin
- Create and run an Empty Activity project in Android Studio.
- Know Kotlin functions, String/Int/Boolean values, if expressions, and basic arithmetic.
- Recognize Text, Button, Column, modifiers, and rememberSaveable from Phase 1.
By the end, you can…
- Use a saved counter for a bounded number of study blocks.
- Toggle a saved Boolean with an ordinary Button.
- Calculate breaks between blocks, including the zero-block case.
- Separate a time estimate from a running timer while keeping the app small.
Required features
- Start with one 25-minute study block and breaks included.
- Add and remove study blocks within 0–8.
- Include or exclude 5-minute breaks through a clearly labelled button.
- Count breaks only between blocks, with no break after the final block.
- Display study minutes, break count, break minutes, and total minutes.
- Reset the plan and restore block and break selections after rotation.
- Use one screen, local data, and basic button-driven state without custom animations or advanced app libraries.
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.
Two different saved values
blocks is an Int and includeBreaks is a Boolean. Together they describe the chosen plan. Minutes are calculated values, so changing the Boolean immediately recomputes the summary.
Breaks exist between blocks
Three blocks have two gaps between them. The expression blocks - 1 is used only when breaks are included and blocks is greater than one. Zero and one block therefore have zero breaks, never a negative count.
A button describes its next action
When breaks are included, the button says Exclude breaks. After the tap changes includeBreaks, the status text and button label both update. One Boolean drives the behavior and its explanation.
Restoration and readable layout
rememberSaveable restores the small selected values when the activity is recreated, such as on rotation. It is temporary screen state rather than permanent app data. MiniScreen provides a scrolling Column; full-width labelled buttons remain reachable when the font is large.
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. Launch the single-screen planner
Your task: Open the project, find MainActivity, and trace the host into StudySessionPlanner. Run before changing the block controls.
Open: app/src/main/java/com/mastercoding/miniprojects/studyplan/MainActivity.kt
How it works: MiniScreen supplies the scroll container. This app estimates minutes from a few small values; it does not track elapsed time.
import androidx.compose.ui.unit.dp
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent { MiniTheme { MiniScreen("Study Session Planner") { StudySessionPlanner() } } }
}
}
@Composable
fun StudySessionPlanner() {
var blocks by rememberSaveable { mutableStateOf(1) }
var includeBreaks by rememberSaveable { mutableStateOf(true) }
val studyMinutes = blocks * 25
val breakCount = if (includeBreaks && blocks > 1) blocks - 1 else 0
val breakMinutes = breakCount * 5
val totalMinutes = studyMinutes + breakMinutes
Text("Plan up to eight 25-minute study blocks. Optional 5-minute breaks go between blocks.")
Text("Study blocks: $blocks", style = MaterialTheme.typography.headlineSmall)
Button(onClick = { blocks = (blocks + 1).coerceAtMost(8) }, enabled = blocks < 8,
modifier = Modifier.fillMaxWidth()) { Text("Add a study block") }
Button(onClick = { blocks = (blocks - 1).coerceAtLeast(0) }, enabled = blocks > 0,
modifier = Modifier.fillMaxWidth()) { Text("Remove a study block") }
Text(if (includeBreaks) "Breaks are included" else "Breaks are excluded")
Button(onClick = { includeBreaks = !includeBreaks }, modifier = Modifier.fillMaxWidth()) {Stop and check
Step 2. Choose the saved screen values
Your task: Declare blocks with an initial value of 1 and includeBreaks with an initial value of true.
Open: app/src/main/java/com/mastercoding/miniprojects/studyplan/MainActivity.kt
How it works: Only selections need to be saved. The total can be recalculated after activity recreation from these same two small values.
@Composable
fun StudySessionPlanner() {
var blocks by rememberSaveable { mutableStateOf(1) }
var includeBreaks by rememberSaveable { mutableStateOf(true) }
val studyMinutes = blocks * 25
val breakCount = if (includeBreaks && blocks > 1) blocks - 1 else 0
val breakMinutes = breakCount * 5
val totalMinutes = studyMinutes + breakMinutes
Text("Plan up to eight 25-minute study blocks. Optional 5-minute breaks go between blocks.")
Text("Study blocks: $blocks", style = MaterialTheme.typography.headlineSmall)
Button(onClick = { blocks = (blocks + 1).coerceAtMost(8) }, enabled = blocks < 8,
modifier = Modifier.fillMaxWidth()) { Text("Add a study block") }
Button(onClick = { blocks = (blocks - 1).coerceAtLeast(0) }, enabled = blocks > 0,
modifier = Modifier.fillMaxWidth()) { Text("Remove a study block") }
Text(if (includeBreaks) "Breaks are included" else "Breaks are excluded")
Button(onClick = { includeBreaks = !includeBreaks }, modifier = Modifier.fillMaxWidth()) {
Text(if (includeBreaks) "Exclude breaks" else "Include breaks")
}
Card(modifier = Modifier.fillMaxWidth()) {
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text("Study: $studyMinutes minutes")
Text("Breaks: $breakCount ($breakMinutes minutes)")
Text("Total: $totalMinutes minutes", style = MaterialTheme.typography.headlineSmall)
}
}Stop and check
Step 3. Calculate time and break gaps
Your task: Multiply blocks by 25. Count blocks - 1 breaks only when includeBreaks is true and blocks exceeds one, multiply by 5, and add study and break minutes.
Open: app/src/main/java/com/mastercoding/miniprojects/studyplan/MainActivity.kt
How it works: At three blocks, study is 75 minutes and two breaks add 10 minutes. At zero blocks the condition supplies zero breaks instead of -1.
var includeBreaks by rememberSaveable { mutableStateOf(true) }
val studyMinutes = blocks * 25
val breakCount = if (includeBreaks && blocks > 1) blocks - 1 else 0
val breakMinutes = breakCount * 5
val totalMinutes = studyMinutes + breakMinutes
Text("Plan up to eight 25-minute study blocks. Optional 5-minute breaks go between blocks.")
Text("Study blocks: $blocks", style = MaterialTheme.typography.headlineSmall)
Button(onClick = { blocks = (blocks + 1).coerceAtMost(8) }, enabled = blocks < 8,
modifier = Modifier.fillMaxWidth()) { Text("Add a study block") }
Button(onClick = { blocks = (blocks - 1).coerceAtLeast(0) }, enabled = blocks > 0,
modifier = Modifier.fillMaxWidth()) { Text("Remove a study block") }
Text(if (includeBreaks) "Breaks are included" else "Breaks are excluded")
Button(onClick = { includeBreaks = !includeBreaks }, modifier = Modifier.fillMaxWidth()) {
Text(if (includeBreaks) "Exclude breaks" else "Include breaks")
}
Card(modifier = Modifier.fillMaxWidth()) {
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text("Study: $studyMinutes minutes")
Text("Breaks: $breakCount ($breakMinutes minutes)")
Text("Total: $totalMinutes minutes", style = MaterialTheme.typography.headlineSmall)
}
}
Button(onClick = { blocks = 1; includeBreaks = true }, modifier = Modifier.fillMaxWidth()) {
Text("Reset plan")
}Stop and check
Step 4. Bound Add and Remove actions
Your task: Connect Add a study block and Remove a study block to their callbacks. Clamp at eight and zero, and disable the matching boundary action.
Open: app/src/main/java/com/mastercoding/miniprojects/studyplan/MainActivity.kt
How it works: The enabled state communicates a limit before the tap. The bounded assignment also protects the counter value inside the callback.
Text("Plan up to eight 25-minute study blocks. Optional 5-minute breaks go between blocks.")
Text("Study blocks: $blocks", style = MaterialTheme.typography.headlineSmall)
Button(onClick = { blocks = (blocks + 1).coerceAtMost(8) }, enabled = blocks < 8,
modifier = Modifier.fillMaxWidth()) { Text("Add a study block") }
Button(onClick = { blocks = (blocks - 1).coerceAtLeast(0) }, enabled = blocks > 0,
modifier = Modifier.fillMaxWidth()) { Text("Remove a study block") }
Text(if (includeBreaks) "Breaks are included" else "Breaks are excluded")
Button(onClick = { includeBreaks = !includeBreaks }, modifier = Modifier.fillMaxWidth()) {
Text(if (includeBreaks) "Exclude breaks" else "Include breaks")
}
Card(modifier = Modifier.fillMaxWidth()) {
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text("Study: $studyMinutes minutes")
Text("Breaks: $breakCount ($breakMinutes minutes)")
Text("Total: $totalMinutes minutes", style = MaterialTheme.typography.headlineSmall)
}
}
Button(onClick = { blocks = 1; includeBreaks = true }, modifier = Modifier.fillMaxWidth()) {
Text("Reset plan")
}
Text("The plan survives rotation. This app calculates a plan; it does not run a timer or schedule notifications.")
}
@Preview(showBackground = true)
@Composable
private fun StudySessionPlannerPreview() {Stop and check
Step 5. Toggle and explain the break choice
Your task: Toggle includeBreaks in its button callback. Calculate the status and next action label from that same Boolean.
Open: app/src/main/java/com/mastercoding/miniprojects/studyplan/MainActivity.kt
How it works: No second label state is needed. At three blocks, excluding breaks changes total from 85 to 75 while leaving the block count unchanged.
modifier = Modifier.fillMaxWidth()) { Text("Remove a study block") }
Text(if (includeBreaks) "Breaks are included" else "Breaks are excluded")
Button(onClick = { includeBreaks = !includeBreaks }, modifier = Modifier.fillMaxWidth()) {
Text(if (includeBreaks) "Exclude breaks" else "Include breaks")
}
Card(modifier = Modifier.fillMaxWidth()) {
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text("Study: $studyMinutes minutes")
Text("Breaks: $breakCount ($breakMinutes minutes)")
Text("Total: $totalMinutes minutes", style = MaterialTheme.typography.headlineSmall)
}
}
Button(onClick = { blocks = 1; includeBreaks = true }, modifier = Modifier.fillMaxWidth()) {
Text("Reset plan")
}
Text("The plan survives rotation. This app calculates a plan; it does not run a timer or schedule notifications.")
}
@Preview(showBackground = true)
@Composable
private fun StudySessionPlannerPreview() {
MiniTheme { MiniScreen("Study Session Planner") { StudySessionPlanner() } }
}Stop and check
Step 6. Reset and verify recreation
Your task: Restore one block and included breaks in Reset plan. Inspect the Preview, then rotate after choosing four blocks with breaks excluded.
Open: app/src/main/java/com/mastercoding/miniprojects/studyplan/MainActivity.kt
How it works: The default one-block plan has no break because it has no gap. Rotation should preserve the chosen count and preference without storing a separate total.
}
}
Button(onClick = { blocks = 1; includeBreaks = true }, modifier = Modifier.fillMaxWidth()) {
Text("Reset plan")
}
Text("The plan survives rotation. This app calculates a plan; it does not run a timer or schedule notifications.")
}
@Preview(showBackground = true)
@Composable
private fun StudySessionPlannerPreview() {
MiniTheme { MiniScreen("Study Session Planner") { StudySessionPlanner() } }
}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.studyplan
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("Study Session Planner") { StudySessionPlanner() } } }
}
}
@Composable
fun StudySessionPlanner() {
var blocks by rememberSaveable { mutableStateOf(1) }
var includeBreaks by rememberSaveable { mutableStateOf(true) }
val studyMinutes = blocks * 25
val breakCount = if (includeBreaks && blocks > 1) blocks - 1 else 0
val breakMinutes = breakCount * 5
val totalMinutes = studyMinutes + breakMinutes
Text("Plan up to eight 25-minute study blocks. Optional 5-minute breaks go between blocks.")
Text("Study blocks: $blocks", style = MaterialTheme.typography.headlineSmall)
Button(onClick = { blocks = (blocks + 1).coerceAtMost(8) }, enabled = blocks < 8,
modifier = Modifier.fillMaxWidth()) { Text("Add a study block") }
Button(onClick = { blocks = (blocks - 1).coerceAtLeast(0) }, enabled = blocks > 0,
modifier = Modifier.fillMaxWidth()) { Text("Remove a study block") }
Text(if (includeBreaks) "Breaks are included" else "Breaks are excluded")
Button(onClick = { includeBreaks = !includeBreaks }, modifier = Modifier.fillMaxWidth()) {
Text(if (includeBreaks) "Exclude breaks" else "Include breaks")
}
Card(modifier = Modifier.fillMaxWidth()) {
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text("Study: $studyMinutes minutes")
Text("Breaks: $breakCount ($breakMinutes minutes)")
Text("Total: $totalMinutes minutes", style = MaterialTheme.typography.headlineSmall)
}
}
Button(onClick = { blocks = 1; includeBreaks = true }, modifier = Modifier.fillMaxWidth()) {
Text("Reset plan")
}
Text("The plan survives rotation. This app calculates a plan; it does not run a timer or schedule notifications.")
}
@Preview(showBackground = true)
@Composable
private fun StudySessionPlannerPreview() {
MiniTheme { MiniScreen("Study Session Planner") { StudySessionPlanner() } }
}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 a fresh screen. | One block gives 25 study minutes, zero breaks, and a 25-minute total; the button offers Exclude breaks. |
| Add until there are three blocks. | Study is 75 minutes, breaks are 2 (10 minutes), and total is 85 minutes. |
| Tap Exclude breaks, then Include breaks. | The total changes to 75 and then back to 85; the block count stays 3. |
| Remove down to zero. | Study, breaks, and total are zero; Remove is disabled and no negative break appears. |
| Add up to eight blocks with breaks included. | Add is disabled at 8; study is 200 minutes, breaks are 7 (35 minutes), and total is 235 minutes. |
| Choose four blocks without breaks, rotate, then tap Reset plan. | Rotation restores four blocks, excluded breaks, and 100 minutes; Reset returns one block with included breaks and 25 minutes. |
| Use a narrow emulator, a large font size, and TalkBack. Scroll through the whole screen. | Labels and results wrap without clipping, controls remain reachable, and each action has a useful spoken text label. |
Your completion checklist
Check an item only after demonstrating it. Progress is saved in this browser.
Troubleshooting: understand the cause
Zero blocks shows a negative break count.
Cause: blocks - 1 was used without the zero/one guard.
Repair: Return zero breaks unless breaks are included and blocks is greater than one.
There is a break after the final study block.
Cause: The formula used blocks instead of blocks - 1 for the gap count.
Repair: Count only the gaps between the selected blocks.
The button offers the action already applied.
Cause: Its label does not read the same includeBreaks value.
Repair: Derive the next action text from includeBreaks each time the screen is described.
Values return to their initial selections after rotation.
Cause: Ordinary local variables or remember were used for the screen selections.
Repair: Use rememberSaveable for the small source values; keep calculated results as ordinary val expressions.
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 do three study blocks have only two breaks?
- Why are minutes calculated values rather than additional saved state?
- What state changes when the break button is tapped, and which text then changes?
- How does restoring the current screen after rotation differ from permanently storing 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
- Show the total as whole hours and remaining minutes using division and remainder.
- Change the fixed study duration from 25 to 20 minutes and update the acceptance examples.
Choose one extension, write its acceptance check first, then implement it without breaking the original scenarios.