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.

Create a free account to save this page, take notes and track your progress. Sign in

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:

The counter's label logic
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 = 0 inside a composable resets on every recomposition and never triggers one. Use remember { mutableIntStateOf(0) } (or rememberSaveable to 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 Modifier parameter. Reusable composables take modifier: Modifier = Modifier and apply it to their root, so callers can position them.
  • Hard-coding colours. Use MaterialTheme.colorScheme so dark mode and dynamic colour work.

Key points

  • Compose UIs are Kotlin functions marked @Composable that describe the UI for the current state.
  • setContent { } in MainActivity hosts the Compose UI; Scaffold provides the standard screen structure.
  • State lives in remember/rememberSaveable; changing it triggers recomposition.
  • Use @Preview for 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.

Was this page helpful?