Phase 1 · Getting Started with ComposeProject 8

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.

← 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

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…

  • 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

  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

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.

TOPIC 02

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.

TOPIC 03

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.

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

MainActivity.kt
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()) {
Reference excerpt · lines 21–46. The full file, with imports, is in Complete source files.

Stop and check

The default result is $0.00 saved and a final price of $10.00.

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.

MainActivity.kt
@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)
        }
    }
Reference excerpt · lines 30–55. The full file, with imports, is in Complete source files.

Stop and check

The selected price and rate each have exactly one source value.

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.

MainActivity.kt
    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")
Reference excerpt · lines 32–57. The full file, with imports, is in Complete source files.

Stop and check

The result card immediately shows the correct saving and final amount for every preset.

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.

MainActivity.kt
    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.
Reference excerpt · lines 37–62. The full file, with imports, is in Complete source files.

Stop and check

Changing either preset updates the result without a Calculate button.

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.

MainActivity.kt

// 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() } }
}
Reference excerpt · lines 61–70. The full file, with imports, is in Complete source files.

Stop and check

Amounts consistently show two decimal digits, including $7.50 and $0.00.

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.

MainActivity.kt
        }
    }
    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() } }
}
Reference excerpt · lines 54–70. The full file, with imports, is in Complete source files.

Stop and check

Reset gives $10.00 at 0%; rotation preserves the chosen $50.00 at 25% and its $37.50 final result.

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

Project acceptance checklist0 / 7 verified

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

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

Further reading