← Back to Prcatice more · Phase 1
Your assignment: Discount Calculator
Choose a sample price and discount with buttons, then show the saving and final amount. Practice simple arithmetic, integer cents, saved selections, and readable result cards.
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…
- Connect fixed price and discount buttons to saved Int state.
- Calculate savings and final amounts from two source values.
- Format integer cents with exactly two decimal digits.
- Verify all preset combinations without a keyboard or external service.
Required features
- Offer sample prices of $10.00, $20.00, and $50.00.
- Offer discount rates of 0%, 10%, 20%, and 25% using ordinary buttons.
- Show selected price and rate, saving, and final price after every choice.
- Store prices in cents and format each amount with two decimal digits.
- Reset to $10.00 and 0%; restore both selections after rotation.
- Clearly label sample US dollar prices and exclude tax.
- 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.
Integer money values
The price is stored as 1000, 2000, or 5000 cents. Every supplied price and rate produces a whole number of cents, so ordinary Int arithmetic gives an exact result for this exercise. Adding arbitrary prices would require an explicit rounding policy.
Calculate in the right order
Multiply priceCents by discountPercent before dividing by 100. For $10 at 25%, 1000 * 25 / 100 gives 250 cents saved and 750 cents left. Dividing the percentage first would lose its fractional value.
Format without extra state
dollars divides cents into whole dollars and a two-digit remainder. The display string is calculated when needed, so it cannot disagree with the selected numeric value.
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. Open and run the calculator
Your task: Find the activity host and launch the supplied app. Identify the price choices, percentage choices, and result card.
Open: app/src/main/java/com/mastercoding/miniprojects/discount/MainActivity.kt
How it works: The entire exercise is one screen and uses fixed local presets. No parsing, tax service, or payment integration is needed.
import androidx.compose.ui.unit.dp
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent { MiniTheme { MiniScreen("Discount Calculator") { DiscountCalculator() } } }
}
}
@Composable
fun DiscountCalculator() {
var priceCents by rememberSaveable { mutableStateOf(1000) }
var discountPercent by rememberSaveable { mutableStateOf(0) }
val savingCents = priceCents * discountPercent / 100
val finalCents = priceCents - savingCents
Text("Choose a sample price and a discount. Amounts are sample US dollars; tax is excluded.")
Text("Price: ${dollars(priceCents)}", style = MaterialTheme.typography.titleMedium)
listOf(1000, 2000, 5000).forEach { cents ->
Button(onClick = { priceCents = cents }, modifier = Modifier.fillMaxWidth()) {
Text(if (priceCents == cents) "Selected price: ${dollars(cents)}" else "Price: ${dollars(cents)}")
}
}
Text("Discount: $discountPercent%", style = MaterialTheme.typography.titleMedium)
listOf(0, 10, 20, 25).forEach { percent ->
Button(onClick = { discountPercent = percent }, modifier = Modifier.fillMaxWidth()) {Stop and check
Step 2. Store the selected price and rate
Your task: Use saved Int state for priceCents and discountPercent. Start with 1000 cents and zero percent.
Open: app/src/main/java/com/mastercoding/miniprojects/discount/MainActivity.kt
How it works: Numbers remain numeric until display. A separate Boolean per button could allow conflicting selections, while one Int identifies the selected preset.
@Composable
fun DiscountCalculator() {
var priceCents by rememberSaveable { mutableStateOf(1000) }
var discountPercent by rememberSaveable { mutableStateOf(0) }
val savingCents = priceCents * discountPercent / 100
val finalCents = priceCents - savingCents
Text("Choose a sample price and a discount. Amounts are sample US dollars; tax is excluded.")
Text("Price: ${dollars(priceCents)}", style = MaterialTheme.typography.titleMedium)
listOf(1000, 2000, 5000).forEach { cents ->
Button(onClick = { priceCents = cents }, modifier = Modifier.fillMaxWidth()) {
Text(if (priceCents == cents) "Selected price: ${dollars(cents)}" else "Price: ${dollars(cents)}")
}
}
Text("Discount: $discountPercent%", style = MaterialTheme.typography.titleMedium)
listOf(0, 10, 20, 25).forEach { percent ->
Button(onClick = { discountPercent = percent }, modifier = Modifier.fillMaxWidth()) {
Text(if (discountPercent == percent) "Selected discount: $percent%" else "Discount: $percent%")
}
}
Card(modifier = Modifier.fillMaxWidth()) {
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text("You save: ${dollars(savingCents)}")
Text("Final price: ${dollars(finalCents)}", style = MaterialTheme.typography.headlineSmall)
}
}Stop and check
Step 3. Calculate the two amounts
Your task: Calculate savingCents by multiplying price by percentage and dividing by 100; subtract that saving to calculate finalCents.
Open: app/src/main/java/com/mastercoding/miniprojects/discount/MainActivity.kt
How it works: All allowed presets have exact cent results. For 5000 cents at 25%, saving is 1250 cents and final is 3750 cents. These values are derived rather than saved.
var priceCents by rememberSaveable { mutableStateOf(1000) }
var discountPercent by rememberSaveable { mutableStateOf(0) }
val savingCents = priceCents * discountPercent / 100
val finalCents = priceCents - savingCents
Text("Choose a sample price and a discount. Amounts are sample US dollars; tax is excluded.")
Text("Price: ${dollars(priceCents)}", style = MaterialTheme.typography.titleMedium)
listOf(1000, 2000, 5000).forEach { cents ->
Button(onClick = { priceCents = cents }, modifier = Modifier.fillMaxWidth()) {
Text(if (priceCents == cents) "Selected price: ${dollars(cents)}" else "Price: ${dollars(cents)}")
}
}
Text("Discount: $discountPercent%", style = MaterialTheme.typography.titleMedium)
listOf(0, 10, 20, 25).forEach { percent ->
Button(onClick = { discountPercent = percent }, modifier = Modifier.fillMaxWidth()) {
Text(if (discountPercent == percent) "Selected discount: $percent%" else "Discount: $percent%")
}
}
Card(modifier = Modifier.fillMaxWidth()) {
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text("You save: ${dollars(savingCents)}")
Text("Final price: ${dollars(finalCents)}", style = MaterialTheme.typography.headlineSmall)
}
}
Button(onClick = { priceCents = 1000; discountPercent = 0 }, modifier = Modifier.fillMaxWidth()) {
Text("Reset price and discount")Stop and check
Step 4. Connect the preset buttons
Your task: Loop over the three price presets and assign the clicked value to priceCents. Do the same for the percentage presets and use text to mark the selected value.
Open: app/src/main/java/com/mastercoding/miniprojects/discount/MainActivity.kt
How it works: These are small fixed button groups in the scrolling screen. Selecting the price does not replace the chosen discount, so two independent choices work together.
Text("Choose a sample price and a discount. Amounts are sample US dollars; tax is excluded.")
Text("Price: ${dollars(priceCents)}", style = MaterialTheme.typography.titleMedium)
listOf(1000, 2000, 5000).forEach { cents ->
Button(onClick = { priceCents = cents }, modifier = Modifier.fillMaxWidth()) {
Text(if (priceCents == cents) "Selected price: ${dollars(cents)}" else "Price: ${dollars(cents)}")
}
}
Text("Discount: $discountPercent%", style = MaterialTheme.typography.titleMedium)
listOf(0, 10, 20, 25).forEach { percent ->
Button(onClick = { discountPercent = percent }, modifier = Modifier.fillMaxWidth()) {
Text(if (discountPercent == percent) "Selected discount: $percent%" else "Discount: $percent%")
}
}
Card(modifier = Modifier.fillMaxWidth()) {
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text("You save: ${dollars(savingCents)}")
Text("Final price: ${dollars(finalCents)}", style = MaterialTheme.typography.headlineSmall)
}
}
Button(onClick = { priceCents = 1000; discountPercent = 0 }, modifier = Modifier.fillMaxWidth()) {
Text("Reset price and discount")
}
Text("Selections survive rotation. This is a small arithmetic exercise with fixed choices.")
}
// All supplied prices and percentages produce a whole number of cents.Stop and check
Step 5. Format the cent amounts
Your task: Implement dollars by combining whole dollars with a remainder padded to two characters. Use it for the selected price, saving, and final amount.
Open: app/src/main/java/com/mastercoding/miniprojects/discount/MainActivity.kt
How it works: For 750 cents the whole part is 7 and remainder is 50, producing $7.50. For zero cents the padded remainder produces $0.00. Only nonnegative preset results reach this helper.
// All supplied prices and percentages produce a whole number of cents.
private fun dollars(cents: Int): String = "$" + (cents / 100) + "." + (cents % 100).toString().padStart(2, '0')
@Preview(showBackground = true)
@Composable
private fun DiscountCalculatorPreview() {
MiniTheme { MiniScreen("Discount Calculator") { DiscountCalculator() } }
}Stop and check
Step 6. Reset, preview, and rotate
Your task: Use Reset price and discount to restore 1000 and zero. Open DiscountCalculatorPreview, then run the app with $50 and 25%, rotate, and inspect the restored values.
Open: app/src/main/java/com/mastercoding/miniprojects/discount/MainActivity.kt
How it works: The reset updates the two source values; all result strings follow automatically. The preview checks composition, while the runtime check verifies rotation and scrolling.
}
}
Button(onClick = { priceCents = 1000; discountPercent = 0 }, modifier = Modifier.fillMaxWidth()) {
Text("Reset price and discount")
}
Text("Selections survive rotation. This is a small arithmetic exercise with fixed choices.")
}
// All supplied prices and percentages produce a whole number of cents.
private fun dollars(cents: Int): String = "$" + (cents / 100) + "." + (cents % 100).toString().padStart(2, '0')
@Preview(showBackground = true)
@Composable
private fun DiscountCalculatorPreview() {
MiniTheme { MiniScreen("Discount Calculator") { DiscountCalculator() } }
}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.discount
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("Discount Calculator") { DiscountCalculator() } } }
}
}
@Composable
fun DiscountCalculator() {
var priceCents by rememberSaveable { mutableStateOf(1000) }
var discountPercent by rememberSaveable { mutableStateOf(0) }
val savingCents = priceCents * discountPercent / 100
val finalCents = priceCents - savingCents
Text("Choose a sample price and a discount. Amounts are sample US dollars; tax is excluded.")
Text("Price: ${dollars(priceCents)}", style = MaterialTheme.typography.titleMedium)
listOf(1000, 2000, 5000).forEach { cents ->
Button(onClick = { priceCents = cents }, modifier = Modifier.fillMaxWidth()) {
Text(if (priceCents == cents) "Selected price: ${dollars(cents)}" else "Price: ${dollars(cents)}")
}
}
Text("Discount: $discountPercent%", style = MaterialTheme.typography.titleMedium)
listOf(0, 10, 20, 25).forEach { percent ->
Button(onClick = { discountPercent = percent }, modifier = Modifier.fillMaxWidth()) {
Text(if (discountPercent == percent) "Selected discount: $percent%" else "Discount: $percent%")
}
}
Card(modifier = Modifier.fillMaxWidth()) {
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text("You save: ${dollars(savingCents)}")
Text("Final price: ${dollars(finalCents)}", style = MaterialTheme.typography.headlineSmall)
}
}
Button(onClick = { priceCents = 1000; discountPercent = 0 }, modifier = Modifier.fillMaxWidth()) {
Text("Reset price and discount")
}
Text("Selections survive rotation. This is a small arithmetic exercise with fixed choices.")
}
// All supplied prices and percentages produce a whole number of cents.
private fun dollars(cents: Int): String = "$" + (cents / 100) + "." + (cents % 100).toString().padStart(2, '0')
@Preview(showBackground = true)
@Composable
private fun DiscountCalculatorPreview() {
MiniTheme { MiniScreen("Discount Calculator") { DiscountCalculator() } }
}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 with the defaults. | Price is $10.00, discount is 0%, saving is $0.00, and final price is $10.00. |
| Choose $10.00 and 25%. | Saving is $2.50 and final price is $7.50. |
| Choose $20.00 and 10%. | Saving is $2.00 and final price is $18.00. |
| Choose $50.00 and 25%, then rotate. | Both selections remain, saving is $12.50, and final price is $37.50. |
| Press Reset price and discount. | Price returns to $10.00, rate to 0%, saving to $0.00, and final price to $10.00. |
| Try all twelve price/rate combinations. | Each final amount equals selected price minus displayed saving, and every amount has two decimal digits. |
| 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
Every nonzero discount still saves zero.
Cause: The percentage was divided by 100 using Int arithmetic before multiplication.
Repair: Multiply priceCents by discountPercent first, then divide by 100.
The result displays $7.5 or $0.0.
Cause: The remainder was displayed without two-digit padding.
Repair: Pad the cent remainder to two digits in dollars.
A displayed result belongs to an earlier selection.
Cause: The result was stored as mutable state and not updated by every button.
Repair: Derive savingCents and finalCents directly from priceCents and discountPercent.
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 using cents keep these preset calculations exact?
- What goes wrong if discountPercent / 100 runs first?
- Which two values must be saved, and which amounts can be recalculated?
- 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
- Add a $40.00 preset and verify it at all four rates.
- Display a sentence explaining the selected arithmetic in cents.
Choose one extension, write its acceptance check first, then implement it without breaking the original scenarios.