Phase 1 · Getting Started with ComposeProject 10

Snack Order

Build a practice snack order with two quantity controls and an automatic total. Practice reusable composables, bounded counters, conditional empty-state text, and simple price arithmetic.

← Back to Prcatice more · Phase 1

Your assignment: Snack Order

Build a practice snack order with two quantity controls and an automatic total. Practice reusable composables, bounded counters, conditional empty-state text, and simple price arithmetic.

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…

  • Render two products with one reusable quantity component.
  • Change saved quantity values independently with Add and Remove buttons.
  • Calculate subtotals, item count, and total from the quantities.
  • Show an empty-order message and reset both values.

Required features

  • Show a sandwich priced at $4 and a juice priced at $2 in sample US dollars.
  • Start with both quantities at zero and an empty-order message.
  • Allow each quantity to change independently within 0–10.
  • Display each product subtotal, combined item count, and order total.
  • Clear both quantities with one button and restore them after rotation.
  • Keep the project a local practice order with no checkout or purchase action.
  • 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

Reusable presentation with callbacks

SnackQuantity receives a product name, quantity, unit price, and two callbacks. It renders the same controls for both products while SnackOrder owns the independent quantities.

TOPIC 02

Totals come from quantities

sandwichTotal is sandwiches * 4 and juiceTotal is juices * 2. Their sum is the total, while adding the quantities gives the item count. Only quantities are mutable; all amounts can be recalculated.

TOPIC 03

Empty is an ordinary state

When itemCount equals zero, an if expression includes the empty-order Text in the composition. Adding either product makes the condition false; clearing the order makes it true again.

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 local order screen

Your task: Open MainActivity.kt and trace the activity into SnackOrder. Identify the two products and summary before adding behavior.

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

How it works: The prices and products are local sample data. MiniScreen supplies vertical spacing and scrolling for the small form.

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

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent { MiniTheme { MiniScreen("Snack Order") { SnackOrder() } } }
    }
}

@Composable
fun SnackOrder() {
    var sandwiches by rememberSaveable { mutableStateOf(0) }
    var juices by rememberSaveable { mutableStateOf(0) }
    val sandwichTotal = sandwiches * 4
    val juiceTotal = juices * 2
    val itemCount = sandwiches + juices
    val totalDollars = sandwichTotal + juiceTotal

    Text("Build a practice order. Sandwiches cost $4 and juices cost $2. Each quantity is limited to 10.")
    SnackQuantity("Sandwich", sandwiches, 4,
        { sandwiches = (sandwiches + 1).coerceAtMost(10) },
        { sandwiches = (sandwiches - 1).coerceAtLeast(0) })
    SnackQuantity("Juice", juices, 2,
        { juices = (juices + 1).coerceAtMost(10) },
        { juices = (juices - 1).coerceAtLeast(0) })
    Card(modifier = Modifier.fillMaxWidth()) {
Reference excerpt · lines 21–46. The full file, with imports, is in Complete source files.

Stop and check

The initial order has zero items, a $0 total, and an empty-order message.

Step 2. Save independent quantities

Your task: Declare sandwiches and juices as saved Int values initialized to zero.

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

How it works: Two products need two source values. Each can be restored on rotation without introducing a collection or storage library.

MainActivity.kt
@Composable
fun SnackOrder() {
    var sandwiches by rememberSaveable { mutableStateOf(0) }
    var juices by rememberSaveable { mutableStateOf(0) }
    val sandwichTotal = sandwiches * 4
    val juiceTotal = juices * 2
    val itemCount = sandwiches + juices
    val totalDollars = sandwichTotal + juiceTotal

    Text("Build a practice order. Sandwiches cost $4 and juices cost $2. Each quantity is limited to 10.")
    SnackQuantity("Sandwich", sandwiches, 4,
        { sandwiches = (sandwiches + 1).coerceAtMost(10) },
        { sandwiches = (sandwiches - 1).coerceAtLeast(0) })
    SnackQuantity("Juice", juices, 2,
        { juices = (juices + 1).coerceAtMost(10) },
        { juices = (juices - 1).coerceAtLeast(0) })
    Card(modifier = Modifier.fillMaxWidth()) {
        Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
            Text("Items: $itemCount")
            Text("Sandwich subtotal: $$sandwichTotal")
            Text("Juice subtotal: $$juiceTotal")
            Text("Order total: $$totalDollars", style = MaterialTheme.typography.headlineSmall)
            if (itemCount == 0) Text("Your practice order is empty.")
        }
    }
    Button(onClick = { sandwiches = 0; juices = 0 }, modifier = Modifier.fillMaxWidth()) {
Reference excerpt · lines 30–55. The full file, with imports, is in Complete source files.

Stop and check

Adding one sandwich leaves the juice quantity at zero.

Step 3. Calculate the summary

Your task: Calculate product subtotals, itemCount, and totalDollars from the two quantities. Render those values in a Card.

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

How it works: For two sandwiches and three juices, the subtotals are $8 and $6, item count is 5, and total is $14. No separate mutable total is needed.

MainActivity.kt
    var sandwiches by rememberSaveable { mutableStateOf(0) }
    var juices by rememberSaveable { mutableStateOf(0) }
    val sandwichTotal = sandwiches * 4
    val juiceTotal = juices * 2
    val itemCount = sandwiches + juices
    val totalDollars = sandwichTotal + juiceTotal

    Text("Build a practice order. Sandwiches cost $4 and juices cost $2. Each quantity is limited to 10.")
    SnackQuantity("Sandwich", sandwiches, 4,
        { sandwiches = (sandwiches + 1).coerceAtMost(10) },
        { sandwiches = (sandwiches - 1).coerceAtLeast(0) })
    SnackQuantity("Juice", juices, 2,
        { juices = (juices + 1).coerceAtMost(10) },
        { juices = (juices - 1).coerceAtLeast(0) })
    Card(modifier = Modifier.fillMaxWidth()) {
        Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
            Text("Items: $itemCount")
            Text("Sandwich subtotal: $$sandwichTotal")
            Text("Juice subtotal: $$juiceTotal")
            Text("Order total: $$totalDollars", style = MaterialTheme.typography.headlineSmall)
            if (itemCount == 0) Text("Your practice order is empty.")
        }
    }
    Button(onClick = { sandwiches = 0; juices = 0 }, modifier = Modifier.fillMaxWidth()) {
        Text("Clear order")
    }
Reference excerpt · lines 32–57. The full file, with imports, is in Complete source files.

Stop and check

Every quantity change immediately produces matching subtotals and a correct total.

Step 4. Reuse a quantity control

Your task: Write SnackQuantity with the product information and Add/Remove callbacks. Use it once for sandwiches and once for juices. Clamp updates to 0–10 and disable boundary actions.

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

How it works: The product label appears in each action, so a screen reader can distinguish Add Sandwich from Add Juice. Full-width buttons keep the labels readable on narrow screens.

MainActivity.kt

@Composable
private fun SnackQuantity(name: String, quantity: Int, unitPrice: Int, add: () -> Unit, remove: () -> Unit) {
    Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
        Text("$name: $quantity at $$unitPrice each", style = MaterialTheme.typography.titleMedium)
        Button(onClick = add, enabled = quantity < 10, modifier = Modifier.fillMaxWidth()) {
            Text("Add $name")
        }
        Button(onClick = remove, enabled = quantity > 0, modifier = Modifier.fillMaxWidth()) {
            Text("Remove $name")
        }
    }
}

@Preview(showBackground = true)
@Composable
private fun SnackOrderPreview() {
    MiniTheme { MiniScreen("Snack Order") { SnackOrder() } }
}
Reference excerpt · lines 60–79. The full file, with imports, is in Complete source files.

Stop and check

The two controls look consistent, update the correct quantity, and respect zero and ten.

Step 5. Show an empty-order message

Your task: Add the empty message only when itemCount equals zero. Try adding either product, then removing it again.

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

How it works: The message follows the same quantities as the summary. There is no separate empty Boolean that could disagree with the order.

MainActivity.kt
            Text("Juice subtotal: $$juiceTotal")
            Text("Order total: $$totalDollars", style = MaterialTheme.typography.headlineSmall)
            if (itemCount == 0) Text("Your practice order is empty.")
        }
    }
    Button(onClick = { sandwiches = 0; juices = 0 }, modifier = Modifier.fillMaxWidth()) {
        Text("Clear order")
    }
    Text("Quantities survive rotation. Prices are sample US dollars with no tax. No purchase is placed.")
}

@Composable
private fun SnackQuantity(name: String, quantity: Int, unitPrice: Int, add: () -> Unit, remove: () -> Unit) {
    Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
        Text("$name: $quantity at $$unitPrice each", style = MaterialTheme.typography.titleMedium)
        Button(onClick = add, enabled = quantity < 10, modifier = Modifier.fillMaxWidth()) {
            Text("Add $name")
        }
        Button(onClick = remove, enabled = quantity > 0, modifier = Modifier.fillMaxWidth()) {
            Text("Remove $name")
        }
    }
}

@Preview(showBackground = true)
@Composable
Reference excerpt · lines 50–75. The full file, with imports, is in Complete source files.

Stop and check

The message disappears after any addition and returns only when both quantities are zero.

Step 6. Clear and verify the saved order

Your task: Set sandwiches and juices to zero inside Clear order. Inspect the supplied Preview, then rotate an order containing two sandwiches and three juices before clearing it.

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

How it works: Rotation restores the quantities, so the calculated $14 summary is restored too. Clearing both values restores the complete initial state.

MainActivity.kt
        }
    }
    Button(onClick = { sandwiches = 0; juices = 0 }, modifier = Modifier.fillMaxWidth()) {
        Text("Clear order")
    }
    Text("Quantities survive rotation. Prices are sample US dollars with no tax. No purchase is placed.")
}

@Composable
private fun SnackQuantity(name: String, quantity: Int, unitPrice: Int, add: () -> Unit, remove: () -> Unit) {
    Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
        Text("$name: $quantity at $$unitPrice each", style = MaterialTheme.typography.titleMedium)
        Button(onClick = add, enabled = quantity < 10, modifier = Modifier.fillMaxWidth()) {
            Text("Add $name")
        }
        Button(onClick = remove, enabled = quantity > 0, modifier = Modifier.fillMaxWidth()) {
            Text("Remove $name")
        }
    }
}

@Preview(showBackground = true)
@Composable
private fun SnackOrderPreview() {
    MiniTheme { MiniScreen("Snack Order") { SnackOrder() } }
}
Reference excerpt · lines 53–78. The full file, with imports, is in Complete source files.

Stop and check

The order survives rotation; Clear order gives zero items, zero subtotals, a $0 total, and the empty message.

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.snacks

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("Snack Order") { SnackOrder() } } }
    }
}

@Composable
fun SnackOrder() {
    var sandwiches by rememberSaveable { mutableStateOf(0) }
    var juices by rememberSaveable { mutableStateOf(0) }
    val sandwichTotal = sandwiches * 4
    val juiceTotal = juices * 2
    val itemCount = sandwiches + juices
    val totalDollars = sandwichTotal + juiceTotal

    Text("Build a practice order. Sandwiches cost $4 and juices cost $2. Each quantity is limited to 10.")
    SnackQuantity("Sandwich", sandwiches, 4,
        { sandwiches = (sandwiches + 1).coerceAtMost(10) },
        { sandwiches = (sandwiches - 1).coerceAtLeast(0) })
    SnackQuantity("Juice", juices, 2,
        { juices = (juices + 1).coerceAtMost(10) },
        { juices = (juices - 1).coerceAtLeast(0) })
    Card(modifier = Modifier.fillMaxWidth()) {
        Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
            Text("Items: $itemCount")
            Text("Sandwich subtotal: $$sandwichTotal")
            Text("Juice subtotal: $$juiceTotal")
            Text("Order total: $$totalDollars", style = MaterialTheme.typography.headlineSmall)
            if (itemCount == 0) Text("Your practice order is empty.")
        }
    }
    Button(onClick = { sandwiches = 0; juices = 0 }, modifier = Modifier.fillMaxWidth()) {
        Text("Clear order")
    }
    Text("Quantities survive rotation. Prices are sample US dollars with no tax. No purchase is placed.")
}

@Composable
private fun SnackQuantity(name: String, quantity: Int, unitPrice: Int, add: () -> Unit, remove: () -> Unit) {
    Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
        Text("$name: $quantity at $$unitPrice each", style = MaterialTheme.typography.titleMedium)
        Button(onClick = add, enabled = quantity < 10, modifier = Modifier.fillMaxWidth()) {
            Text("Add $name")
        }
        Button(onClick = remove, enabled = quantity > 0, modifier = Modifier.fillMaxWidth()) {
            Text("Remove $name")
        }
    }
}

@Preview(showBackground = true)
@Composable
private fun SnackOrderPreview() {
    MiniTheme { MiniScreen("Snack Order") { SnackOrder() } }
}

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.Both quantities and all amounts are zero; both Remove buttons are disabled and the empty message is visible.
Add two sandwiches and three juices.The item count is 5, sandwich subtotal is $8, juice subtotal is $6, and total is $14.
Remove one sandwich.Only sandwiches change: item count is 4 and total is $10; juices remain at 3.
Raise both quantities to ten.Both Add buttons are disabled, item count is 20, and total is $60.
Remove a product down to zero.Its Remove button is disabled and its quantity never becomes negative. The other product keeps its own quantity.
Set two sandwiches and three juices, rotate, then press Clear order.Rotation preserves the five-item $14 order; Clear restores the complete empty state.
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 / 7 verified

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

Troubleshooting: understand the cause

Adding juice changes sandwich quantity.

Cause: The Juice control received the sandwich callback.

Repair: Pass each control its matching saved value and callbacks.

The displayed total does not match the quantities.

Cause: A mutable total was incremented separately or a unit price was omitted.

Repair: Calculate both subtotals and their sum directly from quantities.

The empty message stays after adding a product.

Cause: The condition is fixed or checks only one quantity.

Repair: Use itemCount == 0, where itemCount is the sum of both quantities.

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 SnackQuantity receive callbacks instead of changing parent state by name?
  • What is the difference between itemCount and totalDollars?
  • Why does the empty message not require a saved Boolean?
  • 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

  • Display a simple sentence describing the number of sandwiches and juices.
  • Change one sample unit price and update the worked acceptance calculations.

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

Further reading