You now know enough Kotlin to build screens. In Jetpack Compose a screen is a function that describes what should be visible for the current data. When the data changes, Compose runs the function again and updates only what changed. This idea has a name, recomposition, and it replaces the old way of finding views and changing them by hand.
The pieces of a Compose screen
- Composable functions are marked
@Composable. They can call other composables. - Layouts place things:
Columnstacks vertically,Rowlines up horizontally,Boxstacks on top of each other, andLazyColumnshows long lists efficiently. - Widgets are things the user sees and uses:
Text,Button,OutlinedTextField,Slider,Switch,Card,Checkbox. - Modifiers change how a composable looks and behaves:
Modifier.padding(16.dp).fillMaxWidth(). The order matters, because each modifier wraps the result of the one before it. - Material 3 gives you ready-made, good-looking widgets and a theme with colours and text styles.
State: the most important idea
State is data that can change and that the screen depends on, such as the text in a box or whether a switch is on. In Compose you keep state in a MutableState and read its value in the function. When the value changes, Compose reads it again and redraws. You make state with remember { mutableStateOf(...) }. The remember part keeps the value between redraws. Without it the value would reset to its start every time the function ran.
@Composable
fun Counter() {
var count by remember { mutableIntStateOf(0) }
Button(onClick = { count++ }) {
Text("Clicked $count times")
}
}
Each tap raises count. Compose notices the change, runs Counter again, and the button shows the new text. The by keyword lets you use count like a normal variable. Use mutableIntStateOf, mutableFloatStateOf and mutableStateOf depending on the type.
Project 1: Tip Calculator
The user types a bill, chooses a tip with a slider, picks how many people share it and sees the totals. It teaches state, text input with a number keyboard, a slider, a switch and a card. All the arithmetic lives in a plain Kotlin function, which keeps it separate from the screen and, in lesson 14, easy to test.
Create the project and structure
- Create a new Empty Activity project called
TipCalcwith packagecom.example.tipcalc. - Add three files next to
MainActivity.kt:TipMath.ktandTipScreen.kt. Replace the contents ofMainActivity.ktas shown below. - No extra libraries are needed for this project.
TipCalc/app/src/main/java/com/example/tipcalc/
MainActivity.kt starts the app and shows TipScreen
TipScreen.kt the screen: input, slider, switch, results
TipMath.kt the calculation, with no Android code in it
The code
`app/src/main/java/com/example/tipcalc/MainActivity.kt`
package com.example.tipcalc
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.ui.Modifier
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
MaterialTheme {
Surface(modifier = Modifier.fillMaxSize()) {
TipScreen()
}
}
}
}
}
enableEdgeToEdge() lets the app draw behind the system bars, and systemBarsPadding() later keeps text out from under them. Surface gives a background colour from the theme.
`app/src/main/java/com/example/tipcalc/TipMath.kt`
package com.example.tipcalc
import kotlin.math.ceil
data class TipResult(val tip: Double, val total: Double, val perPerson: Double)
/** Pure function: no Android classes, so it is easy to test (lesson 14). */
fun calculateTip(bill: Double, tipPercent: Int, people: Int, roundUp: Boolean): TipResult {
require(people >= 1) { "people must be at least 1" }
var tip = bill * tipPercent / 100.0
if (roundUp) tip = ceil(tip)
val total = bill + tip
return TipResult(tip = tip, total = total, perPerson = total / people)
}
require checks the input and throws a clear error if it is wrong. Because this function is plain Kotlin you can run it with no phone at all.
`app/src/main/java/com/example/tipcalc/TipScreen.kt`
package com.example.tipcalc
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import java.util.Locale
@Composable
fun TipScreen() {
// State: when any of these change, Compose redraws the parts that read them.
var billText by remember { mutableStateOf("") }
var tipPercent by remember { mutableFloatStateOf(15f) }
var people by remember { mutableIntStateOf(1) }
var roundUp by remember { mutableStateOf(false) }
val bill = billText.toDoubleOrNull() ?: 0.0
val result = calculateTip(bill, tipPercent.toInt(), people, roundUp)
Column(
modifier = Modifier
.fillMaxSize()
.systemBarsPadding()
.padding(20.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Text("Tip Calculator", style = MaterialTheme.typography.headlineMedium)
OutlinedTextField(
value = billText,
onValueChange = { billText = it },
label = { Text("Bill amount") },
singleLine = true,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
modifier = Modifier.fillMaxWidth()
)
Text("Tip: ${tipPercent.toInt()}%")
Slider(
value = tipPercent,
onValueChange = { tipPercent = it },
valueRange = 0f..30f,
steps = 29
)
Row(verticalAlignment = Alignment.CenterVertically) {
Text("People: $people", modifier = Modifier.weight(1f))
OutlinedButton(onClick = { if (people > 1) people-- }) { Text("-") }
Spacer(Modifier.width(8.dp))
OutlinedButton(onClick = { people++ }) { Text("+") }
}
Row(verticalAlignment = Alignment.CenterVertically) {
Text("Round the tip up", modifier = Modifier.weight(1f))
Switch(checked = roundUp, onCheckedChange = { roundUp = it })
}
Card(modifier = Modifier.fillMaxWidth()) {
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(4.dp)) {
Text("Tip: ${money(result.tip)}")
Text("Total: ${money(result.total)}")
Text(
"Each person pays: ${money(result.perPerson)}",
style = MaterialTheme.typography.titleMedium
)
}
}
}
}
private fun money(value: Double): String = String.format(Locale.US, "%.2f", value)
How it works
- Four pieces of state live at the top of
TipScreen: the bill text, tip percent, number of people and the round-up switch. - The bill is kept as text, because the user types it.
toDoubleOrNull()turns it into a number, or null if the box is empty or not a number, and?: 0.0makes that zero. - The result is recalculated on every recomposition. That is cheap here, and it means the totals can never be out of date.
Modifier.weight(1f)makes the label take all spare width so the buttons sit on the right.
Expected output
The screen shows the title, a bill box, a slider at 15%, a people row set to 1, a round-up switch and a card. Type 100 as the bill: the card shows Tip 15.00, Total 115.00 and Each person pays 115.00. Press + twice for three people: each person pays 38.33. Turn on rounding with a bill of 33 and 15%: the tip changes from 4.95 to 5.00.
Your task
- Build the project, run it and check the examples above.
- Add a row of three buttons (10%, 15%, 20%) that set the tip percent in one tap.
- Show the tip percent in a different colour when it is above 25%.
- Keep the bill when you rotate the phone: change
remembertorememberSaveableand test the rotation.
How to check it worked
- Typing 100 gives a total of 115.00 at 15%.
- The numbers update as you move the slider, with no button to press.
- You can explain what
rememberand recomposition do in your own words.