← 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
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…
- 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
- 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.
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.
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.
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.
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. 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.
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()) {Stop and check
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.
@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()) {Stop and check
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.
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")
}Stop and check
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.
@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() } }
}Stop and check
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.
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)
@ComposableStop and check
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.
}
}
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() } }
}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.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 this | Expected 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
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
- 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 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?
| 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
- 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.