Compose flips UI on its head: instead of mutating views, you write functions that describe the UI for the current state. Learn @Composable functions, previews, and the recomposition model.
The old Android UI model was imperative — you built view objects and mutated them (textView.setText). Compose is declarative: a @Composable function describes what the UI should look like for the current data, and when the data changes Compose re-runs the function to produce the new UI. You stop micromanaging widget state and instead express UI as a function of state.
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
// a composable is a function annotated @Composable that EMITS UI
@Composable
fun Greeting(name: String) {
Text(text = "Hello, $name!") // describes UI for the given data
}
// old way (imperative): findViewById -> textView.setText("Hello")
// Compose way (declarative): the UI is a function of 'name'. Change name ->
// Compose re-runs Greeting to produce the updated UI. No manual mutation.An Activity hosts Compose by calling setContent, inside which your composable tree lives. During development, the @Preview annotation renders a composable directly in the IDE without running the app — a fast feedback loop for building UI. You will annotate small composables with @Preview constantly to iterate on their appearance.
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent { // the root of the Compose UI tree
Greeting("Ada")
}
}
}
// render a composable in the IDE WITHOUT launching the app:
@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
Greeting("Ada")
}
// previews give a fast build-see-adjust loop for UI.When state a composable reads changes, Compose recomposes — it re-runs that function (and only the affected parts) to update the UI. Because a composable can run frequently and in any order, it must be free of side effects and not rely on how many times it runs. Grasping "the function re-runs when its inputs change" is the mental model everything else builds on.
Recomposition: when state a composable READS changes,
Compose RE-RUNS that composable to produce the new UI.
state changes -> affected composables re-run -> UI updates
(Compose is smart: it skips composables whose inputs didn't change)
Consequences you must respect:
- a composable can run MANY times, in ANY order, on ANY thread
- so it must be SIDE-EFFECT-FREE (no network calls, no mutating vars directly)
- don't rely on execution count/order
- do side effects through effect APIs (last lesson), not inline
"UI = f(state), re-run on change" is the whole model.Because composables are ordinary Kotlin functions, you build UIs by composing them — calling small composables from bigger ones, passing data as parameters, using if/for to include or repeat UI. This is the "composition" in Compose: reusable pieces assembled into screens, with all the power of Kotlin available inside your UI code.
@Composable
fun UserCard(name: String, isOnline: Boolean) {
Column {
Text(name)
if (isOnline) { // plain Kotlin control flow in the UI
Text("● online")
}
}
}
@Composable
fun UserList(names: List<String>) {
Column {
for (n in names) { // loop to repeat UI
UserCard(name = n, isOnline = true) // compose smaller composables
}
}
}
// build big UIs by COMPOSING small composables + normal Kotlin. That's the idea.