Phase 1 · Getting Started with ComposeProject 7

Phrase Builder

Create a sentence by choosing a subject, action, and place from ordinary buttons. Practice String state, reusable button groups, string templates, and a calculated word count.

← Back to Prcatice more · Phase 1

Your assignment: Phrase Builder

Create a sentence by choosing a subject, action, and place from ordinary buttons. Practice String state, reusable button groups, string templates, and a calculated word count.

Phase 1 · Simple Compose practice

Build one screen using layouts, text, buttons, local resources and small saved state values. These projects use no animation APIs, navigation, databases or background work.

Beginner · 1–2 hours of building

The download is a complete reference app. Read the brief, predict the result, then implement each step in a separate working copy. Code runs in Android Studio on a device or emulator; the website displays and explains the source.

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…

  • Arrange a sentence card and three reusable choice groups.
  • Keep one saved String for each sentence part.
  • Build a sentence and count its words using ordinary Kotlin expressions.
  • Show the current choices with text and restore them after rotation.

Required features

  • Start with the sentence I read at home. and a word count of 4.
  • Offer three subject choices, three actions, and three places through ordinary buttons.
  • Update the sentence immediately from the selected words.
  • Show the current choice using text, so selection does not depend on color.
  • Reset all three parts and retain the chosen parts across 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

  1. 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.
  2. 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.
  3. Sync: Allow Gradle to download the pinned dependencies. Initial setup requires internet. The app runs offline and needs no backend, login or API key.
  4. Run: In Device Manager create an emulator with API 23 or newer, or connect a device with USB debugging. Select the app run configuration and press Run.
  5. 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.
  6. 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.
PowerShell — in the project folder
.\gradlew.bat :app:assembleDebug :app:testDebugUnitTest
macOS/Linux: chmod +x gradlew, then ./gradlew :app:assembleDebug :app:testDebugUnitTest. Unit tests run without a device; manual UI checks need a device or emulator.

Keep the toolchain consistent

The projects use the existing course’s pinned toolchain: Kotlin 2.2.21, AGP 8.13.2, Gradle 8.13, and Compose BOM 2025.12.01. This app uses basic Compose UI. Its Gradle files include no animation, navigation, database or ViewModel libraries. All build files and the Gradle wrapper are included. Open 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.

TOPIC 01

String state and templates

subject, action, and place are the three independent source values. A string template assembles the sentence, so changing only the place cannot silently replace the subject.

TOPIC 02

Small fixed choices

The supplied lists are local constants with three phrases each. WordChoices renders a short Column of ordinary buttons; no list architecture or input keyboard is required.

TOPIC 03

A calculated word count

Every supplied part uses single spaces with no surrounding spaces. Splitting the final sentence on a space therefore counts the words. The final period stays attached to the last word and does not add another word.

TOPIC 04

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

When a saved state value changes, Compose updates the content that reads it. 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 sentence screen

Your task: Open MainActivity.kt and follow the activity into PhraseBuilder. Keep the supplied MiniTheme and MiniScreen host while building the content.

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

How it works: The host gives the lesson a runnable, scrollable screen. PhraseBuilder contains the values and controls that change the sentence.

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

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent { MiniTheme { MiniScreen("Phrase Builder") { PhraseBuilder() } } }
    }
}

@Composable
fun PhraseBuilder() {
    var subject by rememberSaveable { mutableStateOf("I") }
    var action by rememberSaveable { mutableStateOf("read") }
    var place by rememberSaveable { mutableStateOf("at home") }
    val sentence = "$subject $action $place."
    val wordCount = sentence.split(" ").size

    Text("Choose one part from each group. The sentence updates immediately.")
    Card(modifier = Modifier.fillMaxWidth()) {
        Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
            Text(sentence, style = MaterialTheme.typography.headlineSmall)
            Text("Word count: $wordCount")
        }
    }
    WordChoices("Subject", listOf("I", "We", "My friends"), subject) { subject = it }
    WordChoices("Action", listOf("read", "study", "practice Kotlin"), action) { action = it }
Reference excerpt · lines 21–46. The full file, with imports, is in Complete source files.

Stop and check

The app displays the sentence card and all three choice groups.

Step 2. Name the three saved parts

Your task: Declare subject, action, and place with rememberSaveable, using I, read, and at home as their defaults.

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

How it works: Each state value has one clear job. Strings are small supported saved values, so rotation can restore the selection without adding storage.

MainActivity.kt
@Composable
fun PhraseBuilder() {
    var subject by rememberSaveable { mutableStateOf("I") }
    var action by rememberSaveable { mutableStateOf("read") }
    var place by rememberSaveable { mutableStateOf("at home") }
    val sentence = "$subject $action $place."
    val wordCount = sentence.split(" ").size

    Text("Choose one part from each group. The sentence updates immediately.")
    Card(modifier = Modifier.fillMaxWidth()) {
        Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
            Text(sentence, style = MaterialTheme.typography.headlineSmall)
            Text("Word count: $wordCount")
        }
    }
    WordChoices("Subject", listOf("I", "We", "My friends"), subject) { subject = it }
    WordChoices("Action", listOf("read", "study", "practice Kotlin"), action) { action = it }
    WordChoices("Place", listOf("at home", "at school", "in the library"), place) { place = it }
    Button(onClick = { subject = "I"; action = "read"; place = "at home" },
        modifier = Modifier.fillMaxWidth()) {
        Text("Reset sentence")
    }
    Text("The chosen words survive rotation. There is no permanent sentence history.")
}

@Composable
Reference excerpt · lines 30–55. The full file, with imports, is in Complete source files.

Stop and check

A fresh screen uses I, read, and at home as its chosen parts.

Step 3. Build the sentence and count

Your task: Combine the three selected strings with spaces and a final period. Count words by splitting this fixed-format sentence on a space.

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

How it works: The supplied choices contain no leading or trailing spaces. I read at home. contains four words; My friends practice Kotlin in the library. contains seven. Both outputs are calculated from the saved parts.

MainActivity.kt
    var action by rememberSaveable { mutableStateOf("read") }
    var place by rememberSaveable { mutableStateOf("at home") }
    val sentence = "$subject $action $place."
    val wordCount = sentence.split(" ").size

    Text("Choose one part from each group. The sentence updates immediately.")
    Card(modifier = Modifier.fillMaxWidth()) {
        Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
            Text(sentence, style = MaterialTheme.typography.headlineSmall)
            Text("Word count: $wordCount")
        }
    }
    WordChoices("Subject", listOf("I", "We", "My friends"), subject) { subject = it }
    WordChoices("Action", listOf("read", "study", "practice Kotlin"), action) { action = it }
    WordChoices("Place", listOf("at home", "at school", "in the library"), place) { place = it }
    Button(onClick = { subject = "I"; action = "read"; place = "at home" },
        modifier = Modifier.fillMaxWidth()) {
        Text("Reset sentence")
    }
    Text("The chosen words survive rotation. There is no permanent sentence history.")
}

@Composable
private fun WordChoices(title: String, choices: List<String>, selected: String, choose: (String) -> Unit) {
    Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
        Text("$title: $selected", style = MaterialTheme.typography.titleMedium)
Reference excerpt · lines 33–58. The full file, with imports, is in Complete source files.

Stop and check

The sentence and its count always reflect the current choices.

Step 4. Reuse a choice group

Your task: Write WordChoices with a title, list of choices, selected value, and choose callback. Display the current selection above the buttons and prefix its button with Selected.

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

How it works: Passing the selection and callback keeps the state in PhraseBuilder. Each group uses the same layout while updating a different String. Full-width buttons allow long phrases to wrap.

MainActivity.kt

@Composable
private fun WordChoices(title: String, choices: List<String>, selected: String, choose: (String) -> Unit) {
    Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
        Text("$title: $selected", style = MaterialTheme.typography.titleMedium)
        choices.forEach { choice ->
            Button(onClick = { choose(choice) }, modifier = Modifier.fillMaxWidth()) {
                Text(if (choice == selected) "Selected: $choice" else "Choose: $choice")
            }
        }
    }
}

@Preview(showBackground = true)
@Composable
private fun PhraseBuilderPreview() {
    MiniTheme { MiniScreen("Phrase Builder") { PhraseBuilder() } }
}
Reference excerpt · lines 54–72. The full file, with imports, is in Complete source files.

Stop and check

Choosing an action changes only the action, and Selected marks exactly one choice in each group.

Step 5. Reset every sentence part

Your task: Set all three saved strings back to their defaults in Reset sentence. Test after changing every group.

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

How it works: Resetting only one part would leave a mixed sentence. Assigning all three initial values restores the exact first result without separately resetting the count.

MainActivity.kt
    WordChoices("Action", listOf("read", "study", "practice Kotlin"), action) { action = it }
    WordChoices("Place", listOf("at home", "at school", "in the library"), place) { place = it }
    Button(onClick = { subject = "I"; action = "read"; place = "at home" },
        modifier = Modifier.fillMaxWidth()) {
        Text("Reset sentence")
    }
    Text("The chosen words survive rotation. There is no permanent sentence history.")
}

@Composable
private fun WordChoices(title: String, choices: List<String>, selected: String, choose: (String) -> Unit) {
    Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
        Text("$title: $selected", style = MaterialTheme.typography.titleMedium)
        choices.forEach { choice ->
            Button(onClick = { choose(choice) }, modifier = Modifier.fillMaxWidth()) {
                Text(if (choice == selected) "Selected: $choice" else "Choose: $choice")
            }
        }
    }
}

@Preview(showBackground = true)
@Composable
private fun PhraseBuilderPreview() {
    MiniTheme { MiniScreen("Phrase Builder") { PhraseBuilder() } }
}
Reference excerpt · lines 46–71. The full file, with imports, is in Complete source files.

Stop and check

Reset shows I read at home. with Word count: 4.

Step 6. Preview long choices and rotation

Your task: Inspect PhraseBuilderPreview, then run the app and select My friends, practice Kotlin, and in the library. Rotate and inspect a large font setting.

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

How it works: The longest supplied sentence exercises wrapping. The runtime check verifies that saving restores all three choices, which a static preview cannot prove.

MainActivity.kt
@Preview(showBackground = true)
@Composable
private fun PhraseBuilderPreview() {
    MiniTheme { MiniScreen("Phrase Builder") { PhraseBuilder() } }
}
Reference excerpt · lines 67–72. The full file, with imports, is in Complete source files.

Stop and check

The seven-word sentence survives rotation and all buttons remain reachable by scrolling.

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 hereResponsibility
MainActivity.ktThe launcher Activity installs the Compose UI with setContent.
CommonUi.ktMaterial 3 theme and reusable screen container with scrolling and system insets.
Application Kotlin filesLayouts, button callbacks and simple saved state introduced in the guide.
app/src/main/resImages, text resources, and the launch theme.
app/build.gradle.ktsSDK settings and the exact libraries used by this app.
README.md / TOOLCHAIN.mdStudent 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.

MainActivity.kt
package com.mastercoding.miniprojects.phrases

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("Phrase Builder") { PhraseBuilder() } } }
    }
}

@Composable
fun PhraseBuilder() {
    var subject by rememberSaveable { mutableStateOf("I") }
    var action by rememberSaveable { mutableStateOf("read") }
    var place by rememberSaveable { mutableStateOf("at home") }
    val sentence = "$subject $action $place."
    val wordCount = sentence.split(" ").size

    Text("Choose one part from each group. The sentence updates immediately.")
    Card(modifier = Modifier.fillMaxWidth()) {
        Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
            Text(sentence, style = MaterialTheme.typography.headlineSmall)
            Text("Word count: $wordCount")
        }
    }
    WordChoices("Subject", listOf("I", "We", "My friends"), subject) { subject = it }
    WordChoices("Action", listOf("read", "study", "practice Kotlin"), action) { action = it }
    WordChoices("Place", listOf("at home", "at school", "in the library"), place) { place = it }
    Button(onClick = { subject = "I"; action = "read"; place = "at home" },
        modifier = Modifier.fillMaxWidth()) {
        Text("Reset sentence")
    }
    Text("The chosen words survive rotation. There is no permanent sentence history.")
}

@Composable
private fun WordChoices(title: String, choices: List<String>, selected: String, choose: (String) -> Unit) {
    Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
        Text("$title: $selected", style = MaterialTheme.typography.titleMedium)
        choices.forEach { choice ->
            Button(onClick = { choose(choice) }, modifier = Modifier.fillMaxWidth()) {
                Text(if (choice == selected) "Selected: $choice" else "Choose: $choice")
            }
        }
    }
}

@Preview(showBackground = true)
@Composable
private fun PhraseBuilderPreview() {
    MiniTheme { MiniScreen("Phrase Builder") { PhraseBuilder() } }
}

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 thisExpected result
Launch a fresh screen.The card says I read at home. and Word count: 4; the three default buttons are marked Selected.
Choose We in Subject.The sentence becomes We read at home. and only the subject selection changes.
Choose My friends, practice Kotlin, and in the library.The card says My friends practice Kotlin in the library. with Word count: 7.
Tap the already selected action again.The sentence and count remain unchanged and no duplicate text is added.
Rotate with the seven-word sentence, then press Reset sentence.All choices survive rotation; Reset returns the initial four-word sentence.
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

Project acceptance checklist0 / 6 verified

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

Troubleshooting: understand the cause

The sentence ignores a new choice.

Cause: A card contains fixed sample text instead of reading state.

Repair: Render sentence derived from the three saved parts.

Choosing a place also changes the subject.

Cause: The wrong state is assigned in a group callback.

Repair: Connect each WordChoices call to its matching value and setter.

The word count is too large.

Cause: A new preset includes repeated or surrounding spaces.

Repair: Keep the lesson presets in single-space format, or normalize whitespace before extending the exercise.

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

  1. Submit the Android Studio project without build caches, .gradle or machine-specific local.properties.
  2. Include two screenshots demonstrating normal behavior and an edge case.
  3. Add a short README recording the device/API version, actions, observed outcomes, and any known limitation.
  4. Answer the questions below using function and state names from your own implementation.
  • Why does the sentence not need its own mutable state?
  • How can the same WordChoices function update three different saved values?
  • Why does a multiword choice change the word count by more than one?
  • How does restoring the current screen after rotation differ from permanently storing it?
CriterionPointsWhat to demonstrate
Core behavior40All required actions give the expected result.
Validation and lifecycle20Invalid inputs, empty states, rotation and the project's persistence rules work.
Interface and accessibility20Readable contrast, labeled inputs, meaningful image descriptions, scrollable content and usable touch targets.
Code and explanation20Small named functions, clear state ownership and a correct explanation of one event-to-result trace.

Extra challenges after the core works

  • Add one more subject preset that works with the supplied verbs.
  • Add a second Text showing the sentence character count using sentence.length.

Choose one extension, write its acceptance check first, then implement it without breaking the original scenarios.

Further reading