State is the data that changes what your Compose UI looks like — a counter’s value, whether a menu is open, the list of items fetched from a server. Getting state management right is what separates a Compose app that feels smooth from one that recomposes constantly, forgets input on rotation, or shows stale data. This guide covers the core APIs, in the order you’ll actually run into them.
The Basics: mutableStateOf and remember
Compose re-runs (recomposes) a composable function whenever the state it reads changes. mutableStateOf creates a value Compose knows how to watch; remember tells Compose to keep that value across recompositions instead of resetting it every time the function runs.
@Composable
fun Counter() {
var count by remember { mutableStateOf(0) }
Button(onClick = { count++ }) {
Text("Count: $count")
}
}
This is the starting point for almost everything else in this guide. The catch: plain remember loses its value on a configuration change like screen rotation. For state that needs to survive that, see remember vs rememberSaveable.
Structuring State: Hoisting
Instead of letting every composable own and mutate its own state, Compose’s convention is to move state up to a common caller and pass it down as parameters plus a change callback. This is what makes composables reusable and testable rather than tightly coupled to one screen. See state hoisting in Jetpack Compose for the full pattern.
Connecting to Business Logic: ViewModel and StateFlow
mutableStateOf only exists inside the Compose runtime. For state that represents your app’s actual data — fetched from a repository, shared across screens, tested independent of any UI — you generally want a ViewModel exposing a StateFlow instead. See mutableStateOf vs StateFlow for when to use each, and collectAsStateWithLifecycle for how to read a StateFlow safely inside a composable.
Running Code in Response to State: Side Effects
Composable functions are meant to be free of side effects, but real screens need to launch coroutines, register listeners, or notify non-Compose code. Compose’s effect APIs — LaunchedEffect, DisposableEffect, and SideEffect — are the sanctioned way to do that. See Compose side effects explained for which one fits which situation.
Performance: Stability and Recomposition
Compose can skip recomposing a function if it can prove its inputs didn’t meaningfully change — but only for types it considers stable. Understanding stability, and knowing how to measure recomposition instead of guessing at it, is the difference between a Compose screen that scrolls smoothly and one that jankily re-renders on every frame:
- Compose stability explained — what makes a type stable, and how @Stable/@Immutable fit in.
- Debugging recomposition — how to actually see what’s recomposing and why, instead of guessing.
- derivedStateOf — avoiding wasted recomposition when a value is derived from rapidly-changing state, like scroll position.
A Practical Starting Point
If you’re new to Compose state, the order above is a reasonable learning path: get comfortable with remember/mutableStateOf first, learn to hoist state instead of scattering it across composables, move real app data into a ViewModel with StateFlow once you need to, and only start worrying about stability and recomposition performance once you notice an actual jank problem — it’s easy to over-optimize state you don’t yet have a measured issue with.