Android with Kotlin and Jetpack Compose · Compose Foundations: Screens, Layout, Lists and State
Your First Android App with Jetpack Compose
Create a project in Android Studio, understand MainActivity, setContent and composable functions, preview UI without a device, and run the app on an emulator or your phone.
What you'll build
A "Hello" screen with a button that counts taps, running on an emulator or your own Android phone. You'll create the project, see the code Android Studio generates, preview the UI in the editor, and change it.
The idea
Jetpack Compose is Android's modern UI toolkit and the default for new apps. Instead of XML layouts that you find and update by ID, you write composable functions: Kotlin functions annotated with @Composable that describe the UI for the current data. When the data changes, Compose calls the affected functions again (recomposition) and updates only what changed.
The pieces of a new project:
| Piece | What it does |
|---|---|
MainActivity |
the entry point Android starts; calls setContent { } |
@Composable functions |
describe UI; they can call other composables |
Theme (ui/theme) |
Material 3 colours, typography and shapes, light and dark |
@Preview |
renders a composable in Android Studio without running the app |
app/build.gradle.kts |
SDK versions and dependencies (Compose comes through a BOM) |
The code
In Android Studio choose New Project > Empty Activity. It generates a Compose app; trimmed to the essentials:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge() // draw behind the status and navigation bars
setContent {
MyAppTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
TapCounter(modifier = Modifier.padding(innerPadding))
}
}
}
}
}
@Composable
fun TapCounter(modifier: Modifier = Modifier) {
var taps by rememberSaveable { mutableIntStateOf(0) }
Column(
modifier = modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text("Hello from my phone", style = MaterialTheme.typography.headlineSmall)
Spacer(Modifier.height(16.dp))
Button(onClick = { taps++ }) {
Text(tapLabel(taps))
}
}
}
fun tapLabel(taps: Int) = "Tapped $taps ${if (taps == 1) "time" else "times"}"
@Preview(showBackground = true)
@Composable
fun TapCounterPreview() {
MyAppTheme { TapCounter() }
}
Compose libraries come from the Compose BOM, which keeps their versions compatible:
dependencies {
val composeBom = platform("androidx.compose:compose-bom:2026.09.00")
implementation(composeBom)
implementation("androidx.compose.material3:material3")
implementation("androidx.activity:activity-compose:1.13.0")
debugImplementation("androidx.compose.ui:ui-tooling")
}
Run with the green Run button on an emulator (Device Manager) or a phone with USB or wireless debugging turned on.
Try the logic
Composables should stay thin: the text logic is a plain function, so it runs and tests without a device:
fun tapLabel(taps: Int) = "Tapped $taps ${if (taps == 1) "time" else "times"}"
fun main() {
var taps = 0
println(tapLabel(taps))
taps++
println(tapLabel(taps))
repeat(2) { taps++ }
println(tapLabel(taps))
}
Output
Tapped 0 times Tapped 1 time Tapped 3 times
Run this on your computer — see “Get Started” for setup.
Common mistakes
- Plain variables for state.
var taps = 0inside a composable resets on every recomposition and never triggers one. Useremember { mutableIntStateOf(0) }(orrememberSaveableto survive rotation). - Doing work in a composable body. Composables can run many times; never start network calls or write files directly in them.
- Ignoring the
Modifierparameter. Reusable composables takemodifier: Modifier = Modifierand apply it to their root, so callers can position them. - Hard-coding colours. Use
MaterialTheme.colorSchemeso dark mode and dynamic colour work.
Key points
- Compose UIs are Kotlin functions marked
@Composablethat describe the UI for the current state. setContent { }inMainActivityhosts the Compose UI;Scaffoldprovides the standard screen structure.- State lives in
remember/rememberSaveable; changing it triggers recomposition. - Use
@Previewfor fast iteration and keep logic in plain, testable functions.
Further reading: Compose tutorial, Thinking in Compose.
Practice questions
What is recomposition in Jetpack Compose?
Answer
When state that a composable reads changes, Compose calls that composable again to produce updated UI, and applies only the differences to the screen. Composables should therefore be fast, free of side effects and safe to run many times.
What's the difference between remember and rememberSaveable?
Answer
remember keeps a value across recompositions but loses it when the activity is recreated (for example on rotation) or the process dies. rememberSaveable also saves it in the saved-instance state bundle, so it survives configuration changes and process death (for types that can be saved).
Open in the app to mark lessons complete, take notes and track your progress.