Phase 1 · Getting Started with ComposeModule 15~30 min read

Phase 1 Capstone · Daily Water Counter

Track up to eight glasses of water on one screen. Add a glass, undo one, or reset the count. Practice a single saved UI value and button callbacks; this is a counter exercise, not health advice.

Phase 1: Daily Water Counter

Track up to eight glasses of water on one screen. Add a glass, undo one, or reset the count. Practice a single saved UI value and button callbacks; this is a counter exercise, not health advice.

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

  • Composable functions and previews
  • Column and Row
  • State and rememberSaveable

Application structure and responsibilities

  • One screen with a single main task.

Concepts to explain: Activity entry point · Material 3 and Scaffold padding · Composable functions · rememberSaveable · Events and recomposition · Derived progress · Simple bounds

What you will practice

  • Activity entry point
  • Material 3 and Scaffold padding
  • Composable functions
  • rememberSaveable
  • Events and recomposition
  • Derived progress
  • Simple bounds

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. 1. Open the project and trace the first screen: The emulator shows Daily Water Counter with a count of 0 and no content hidden behind the system bars.
  2. 2. Give the screen one source of truth: The first composition displays 0. Later changes to count update the UI without manually changing a Text view.
  3. 3. Derive the label and progress: At count 3, text reads 3 of 8 and the indicator is approximately 37.5% filled.
  4. 4. Wire the bounded Add and Subtract events: Subtract is disabled at 0, Add is disabled at 8, and both work between those bounds.
  5. 5. Reset and explain the completion message: Reaching 8 shows Goal reached! Reset returns the display, progress, and button states to the initial condition.
  6. 6. Verify restoration and readable feedback: Rotation preserves the count; all actions and messages stay usable with larger text.

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: Open the project and trace the first screen

Download and extract the complete project. In Android Studio, open the folder containing settings.gradle.kts, wait for Gradle sync, choose an API 23+ device, and run app. Read MainActivity from onCreate to setContent. MaterialTheme provides component styles; Scaffold reports the system-bar padding used by CounterScreen.

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

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

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            MaterialTheme {
                Scaffold { innerPadding ->
                    CounterScreen(Modifier.padding(innerPadding))
                }
            }
        }
    }
}

@Composable
fun CounterScreen(modifier: Modifier = Modifier) {
    val goal = 8
Focused excerpt, lines 28–46. Select this file in Complete source code for every line.

Expected behavior

The emulator shows Daily Water Counter with a count of 0 and no content hidden behind the system bars.

Step 2: Give the screen one source of truth

Inside CounterScreen, goal is the fixed value 8 and count is mutable Compose state created with rememberSaveable. Reading count during composition connects the visible output to that state. rememberSaveable can restore this small value after rotation or system recreation; it does not create permanent storage.

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

MainActivity.kt
fun CounterScreen(modifier: Modifier = Modifier) {
    val goal = 8
    var count by rememberSaveable { mutableIntStateOf(0) }

    // English labels keep this first project short. Move them to strings.xml to localize.
    Column(
        modifier = modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(24.dp),
        verticalArrangement = Arrangement.spacedBy(20.dp, Alignment.CenterVertically),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text("Daily Water Counter", style = MaterialTheme.typography.headlineLarge)
        Text("Track up to $goal glasses. You can subtract or start again.")
        Text("$count", style = MaterialTheme.typography.displayLarge)
        Text("Progress: $count of $goal glasses")
        LinearProgressIndicator(
            progress = { count.toFloat() / goal },
            modifier = Modifier.fillMaxWidth()
        )
        Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
Focused excerpt, lines 45–63. Select this file in Complete source code for every line.

Expected behavior

The first composition displays 0. Later changes to count update the UI without manually changing a Text view.

Step 3: Derive the label and progress

The number and progress label interpolate count. LinearProgressIndicator reads count.toFloat() / goal, converting 0–8 into the 0–1 fraction expected by the component. Floating-point conversion avoids integer division. There is no separate mutable progress variable to keep synchronized.

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

MainActivity.kt
        Text("Progress: $count of $goal glasses")
        LinearProgressIndicator(
            progress = { count.toFloat() / goal },
            modifier = Modifier.fillMaxWidth()
        )
        Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
            OutlinedButton(onClick = { count -= 1 }, enabled = count > 0) {
                Text("Subtract")
            }
            Button(onClick = { count += 1 }, enabled = count < goal) {
                Text("Add")
            }
        }
        OutlinedButton(onClick = { count = 0 }, enabled = count > 0) {
            Text("Reset")
        }
        Text(if (count == goal) "Goal reached!" else "Keep going — one glass at a time.")
    }
}
Focused excerpt, lines 58–76. Select this file in Complete source code for every line.

Expected behavior

At count 3, text reads 3 of 8 and the indicator is approximately 37.5% filled.

Step 4: Wire the bounded Add and Subtract events

Add increments count and is enabled only below the goal. Subtract decrements count and is enabled only above zero. The conditions prevent negative numbers and keep the progress fraction in the expected range. Each click writes state; Compose derives the new display.

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

MainActivity.kt
        )
        Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
            OutlinedButton(onClick = { count -= 1 }, enabled = count > 0) {
                Text("Subtract")
            }
            Button(onClick = { count += 1 }, enabled = count < goal) {
                Text("Add")
            }
        }
        OutlinedButton(onClick = { count = 0 }, enabled = count > 0) {
            Text("Reset")
        }
        Text(if (count == goal) "Goal reached!" else "Keep going — one glass at a time.")
    }
}
Focused excerpt, lines 62–77. Select this file in Complete source code for every line.

Expected behavior

Subtract is disabled at 0, Add is disabled at 8, and both work between those bounds.

Step 5: Reset and explain the completion message

Reset assigns 0 to count. The final Text chooses its message from count == goal, so success cannot disagree with the visible number. Reset is disabled when already at zero, giving students another example of deriving UI behavior from one value.

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

MainActivity.kt
            Text("Reset")
        }
        Text(if (count == goal) "Goal reached!" else "Keep going — one glass at a time.")
    }
}
Focused excerpt, lines 72–77. Select this file in Complete source code for every line.

Expected behavior

Reaching 8 shows Goal reached! Reset returns the display, progress, and button states to the initial condition.

Step 6: Verify restoration and readable feedback

Reach 4 and rotate the emulator. The saved count should remain 4. Reach 8 and reset. Increase font size and confirm the scrollable content remains reachable. The progress text communicates the same meaning as the bar. English labels are intentionally inline; moving them to string resources is a localization extension.

Open: app/src/main/java/com/mastercoding/phase1/MainActivity.kt · README.md

MainActivity.kt
    // English labels keep this first project short. Move them to strings.xml to localize.
    Column(
        modifier = modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(24.dp),
        verticalArrangement = Arrangement.spacedBy(20.dp, Alignment.CenterVertically),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text("Daily Water Counter", style = MaterialTheme.typography.headlineLarge)
        Text("Track up to $goal glasses. You can subtract or start again.")
        Text("$count", style = MaterialTheme.typography.displayLarge)
        Text("Progress: $count of $goal glasses")
        LinearProgressIndicator(
            progress = { count.toFloat() / goal },
            modifier = Modifier.fillMaxWidth()
        )
        Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
            OutlinedButton(onClick = { count -= 1 }, enabled = count > 0) {
                Text("Subtract")
            }
            Button(onClick = { count += 1 }, enabled = count < goal) {
Focused excerpt, lines 49–67. Select this file in Complete source code for every line.

Expected behavior

Rotation preserves the count; all actions and messages stay usable with larger text.

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/phase1/MainActivity.ktThe complete app: activity setup, the saved count, derived UI, and all three button handlers in one short Kotlin file.
README.mdProject opening instructions, a build sequence, expected behavior, and small extension exercises.

The complete app: activity setup, the saved count, derived UI, and all three button handlers in one short Kotlin file.

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

MainActivity.kt
package com.mastercoding.phase1

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
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.unit.dp

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            MaterialTheme {
                Scaffold { innerPadding ->
                    CounterScreen(Modifier.padding(innerPadding))
                }
            }
        }
    }
}

@Composable
fun CounterScreen(modifier: Modifier = Modifier) {
    val goal = 8
    var count by rememberSaveable { mutableIntStateOf(0) }

    // English labels keep this first project short. Move them to strings.xml to localize.
    Column(
        modifier = modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(24.dp),
        verticalArrangement = Arrangement.spacedBy(20.dp, Alignment.CenterVertically),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text("Daily Water Counter", style = MaterialTheme.typography.headlineLarge)
        Text("Track up to $goal glasses. You can subtract or start again.")
        Text("$count", style = MaterialTheme.typography.displayLarge)
        Text("Progress: $count of $goal glasses")
        LinearProgressIndicator(
            progress = { count.toFloat() / goal },
            modifier = Modifier.fillMaxWidth()
        )
        Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
            OutlinedButton(onClick = { count -= 1 }, enabled = count > 0) {
                Text("Subtract")
            }
            Button(onClick = { count += 1 }, enabled = count < goal) {
                Text("Add")
            }
        }
        OutlinedButton(onClick = { count = 0 }, enabled = count > 0) {
            Text("Reset")
        }
        Text(if (count == goal) "Goal reached!" else "Keep going — one glass at a time.")
    }
}

Definition of done

Project acceptance checklist0 / 6 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
Launch the app and try Subtract.Count remains 0 because Subtract is disabled; Reset is also disabled.
Tap Add three times, then Subtract once.Count reads 2, the label reads 2 of 8, and the progress fraction is 0.25.
Reach 8 and try Add.Add is disabled and Goal reached! is visible.
Rotate while the count is 4.The recreated activity restores 4 through rememberSaveable.
Tap Reset after increasing the count.Count and progress return to 0, with Subtract and Reset disabled.

Common mistakes and repairs

  • Using count / goal performs integer division; convert count to Float first.
  • A plain local variable is recreated during composition and is not observable Compose state.
  • A separate mutable progress or success flag can disagree with the count; derive both instead.
  • rememberSaveable is small UI-state restoration, not a permanent record of completed practice.

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

  • Change the goal to 20 and predict the progress fraction.
  • Move English text into formatted Android string resources.
  • Extract a bounded increment function and discuss its inputs and output.

After the intervening lessons, continue with Packing Checklist.

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