Jetpack Compose cheat sheet
The 22 Jetpack Compose interview answers in 30 seconds each, plus quick facts. Printable, free.
One page per topic: print it or save it as PDF from your browser's print menu. Practise these questions
Rules of thumb
- Composables must be fast and side-effect free; effects go in effect APIs.
- Hoist state: value down, events up.
- rememberSaveable for small UI state; ViewModel for screen data.
- Read fast-changing state in layout or draw lambdas.
- Give lazy list items stable keys.
- collectAsStateWithLifecycle, not collectAsState, on Android.
Compose vs the View system
With Views you build a tree of objects once and then mutate them (textView.text = ...). With Compose you write functions that describe the UI for the current state, and Compose re-runs the parts whose state changed. The UI can't drift out of sync with state, because there are no view objects to forget to update.
What triggers recomposition?
Recomposition is Compose re-running composable functions to update the UI. It happens when a State object that a composable read during its last run changes. Compose then re-runs the smallest scopes that read it and skips children whose parameters haven't changed. It's driven by state reads, not by anything you call.
remember vs rememberSaveable
remember keeps a value across recompositions but loses it when the composable leaves composition or the Activity is recreated, such as on rotation. rememberSaveable also saves the value into the saved-instance-state Bundle, so it survives rotation and process death. Use it for small UI state like text input or a selected tab.
State hoisting
State hoisting means moving state out of a composable to its caller, passing the value down and events up: value and onValueChange. The composable becomes stateless, so it's reusable, easy to preview and test, and the state has a single owner. Hoist state to the lowest common parent that needs to read or change it.
@Composable
fun Counter(count: Int, onIncrement: () -> Unit, modifier: Modifier = Modifier) {
Button(onClick = onIncrement, modifier = modifier) { Text("Count: $count") }
}Stability and skipping
Compose can skip a composable when its parameters are equal to last time. A type is stable if Compose can trust that equality: primitives, Strings, lambdas, and classes whose public properties are all val of stable types. With strong skipping, which is on by default with the Kotlin 2 Compose compiler, unstable parameters are compared by instance (===), so a new List each time still forces recomposition.
@Composable
fun ParentFixed(query: String, tags: List<String>) {
val visible = remember(tags) { tags.filter { it.isNotBlank() } }
Text(query)
TagRowFixed(tags = visible)
}
@Composable
fun TagRowFixed(tags: List<String>) { Text(tags.joinToString()) }Deferring state reads
Reading scroll state in a composable body makes that scope recompose on every scroll pixel. Defer the read to a later phase: use the lambda form Modifier.offset { IntOffset(0, -state.firstVisibleItemScrollOffset) } or graphicsLayer { }. Then only layout or drawing re-runs, and composition is skipped entirely.
derivedStateOf
Use derivedStateOf when a state changes much more often than the value you derive from it, for example showing a 'Scroll to top' button when the first visible item index is above 0. The derived state only notifies readers when its result changes, so the screen recomposes once when the button appears, not on every scroll event.
LaunchedEffect and its keys
LaunchedEffect(key) { } starts a coroutine when it enters composition and cancels it when it leaves. If any key changes, the running coroutine is cancelled and a new one starts. Use it to run suspend work tied to the composable, such as showing a snackbar or reacting to an ID. It runs on the main thread, in the composition's coroutine context.
DisposableEffect
Use DisposableEffect for non-suspending setup that needs matching cleanup: register a listener or observer when entering composition, unregister in onDispose when leaving or when keys change. LaunchedEffect is for suspend work. The onDispose block is required, which makes forgetting cleanup a compile error.
rememberCoroutineScope
Use rememberCoroutineScope() to launch coroutines from event handlers such as onClick, for example to scroll a list or open a drawer. LaunchedEffect runs because the composition changed; rememberCoroutineScope runs because the user did something. The scope is cancelled when the composable leaves composition.
rememberUpdatedState
A long-running effect captures values when it starts. If a callback parameter changes later, the effect still calls the old one, unless you restart the effect, which may not be wanted, like a splash timeout. rememberUpdatedState(onTimeout) gives the effect a State that always holds the latest value without restarting it.
Keys in LazyColumn
Without keys, items are identified by position. When you insert or remove an item, the state of each row (a checkbox, an expanded flag, remember values) stays at its old position and ends up on the wrong item, and more rows recompose. items(list, key = { it.id }) ties state and animations to the item itself.
data class Row(val id: Long, val title: String)
@Composable
fun KeyedRows(rows: List<Row>) {
LazyColumn {
items(rows, key = { it.id }, contentType = { "row" }) { row ->
Text(row.title, Modifier.animateItem())
}
}
}Why modifier order matters
Each modifier wraps the ones after it, so they apply from the outside in. Modifier.padding(16.dp).background(Color.Red) adds space and then paints the smaller area red; reversed, the red fills the padding too. The same applies to clickable: placed before padding, the touch area includes the padding.
CompositionLocal
A CompositionLocal passes a value implicitly down the tree, so composables deep inside can read it without every level passing it as a parameter. The theme, LocalContext and LocalLifecycleOwner work this way. It's the wrong tool for ordinary screen data or dependencies, because it hides what a composable needs and makes previews and tests surprising.
data class Spacing(val small: Dp = 4.dp, val medium: Dp = 8.dp)
val LocalSpacing = staticCompositionLocalOf { Spacing() }
@Composable
fun SpacedCard(content: @Composable () -> Unit) {
Box(Modifier.padding(LocalSpacing.current.medium)) { content() }
}collectAsStateWithLifecycle
Use val state by viewModel.state.collectAsStateWithLifecycle(). It collects only while the lifecycle is at least STARTED, so nothing runs in the background, and it turns each value into Compose State. collectAsState keeps collecting while the composable is in composition, even when the app is backgrounded.
class NotesListViewModel : ViewModel() {
val notes: StateFlow<List<String>> = MutableStateFlow(listOf("First"))
}Navigation in Compose
Navigation Compose keeps a back stack of destinations in a NavController. Since Navigation 2.8, routes can be @Serializable classes or objects, so arguments are typed: navController.navigate(Profile(id = "42")) and backStackEntry.toRoute<Profile>(). Pass IDs, not whole objects, and load data in the destination's ViewModel.
Previews that scale
Previews render composables in Android Studio without running the app. They work well when screens are split into a stateless content composable that takes plain state and lambdas, and a thin route composable that wires in the ViewModel. Then you preview the content with sample data for each state: loading, empty, error, long text, dark mode and large font.
Slot APIs
A slot API takes composable lambdas as parameters, like TopAppBar(title = { }, actions = { }) or Scaffold(topBar = { }) { padding -> }. The component owns layout and behaviour, while the caller decides what goes in each slot. That's more flexible than dozens of parameters for text, icon and colour.
State lists and mutation
mutableStateOf(mutableListOf()) only notices when you assign a new value. Calling add on the list inside changes the list but not the State, so nothing recomposes. Either replace the list (items = items + new) with an immutable List, or use mutableStateListOf(), whose add and remove are tracked by Compose.
Compose and Views together
Put a ComposeView in the XML or Fragment and call setContent { } on it. Set a ViewCompositionStrategy that matches the owner, such as DisposeOnViewTreeLifecycleDestroyed in Fragments, so the composition is disposed with the view. To host a View inside Compose, use AndroidView(factory = { }, update = { }).
class NotesFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View =
ComposeView(requireContext()).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent { Text("Hello from Compose") }
}
}Strong skipping mode
Strong skipping lets Compose skip composables even when they have unstable parameters: unstable values are compared by instance (===), stable ones by equals. It also remembers lambdas automatically, keyed on what they capture. It became the default with the Kotlin 2.0.20 Compose compiler, so many old stability tricks are no longer needed.
Choosing an animation API
For one value, use animate*AsState (like animateDpAsState). For showing and hiding, use AnimatedVisibility with enter and exit transitions. For swapping content based on state, use AnimatedContent or Crossfade. For several values driven by one state, use updateTransition. For gestures or full control, use Animatable.
Quick facts
- Compose phases
- Compose renders a frame in three phases: composition, layout and drawing.
- @Immutable vs @Stable
- @Immutable promises a value never changes; @Stable promises changes are notified through Compose state.
- Modifier.animateItem()
- animateItem() on a LazyColumn item animates insertions, removals and moves; it needs item keys.
- Compose compiler plugin
- Since Kotlin 2.0, the Compose compiler ships with Kotlin and is applied with the org.jetbrains.kotlin.plugin.compose Gradle plugin.
- SideEffect
- SideEffect { } runs after every successful recomposition, for publishing Compose state to non-Compose code.
- produceState
- produceState launches a coroutine that turns non-Compose data into a State and cancels it when it leaves composition.