Compose State
remember, mutableStateOf, derivedStateOf, state hoisting, ViewModel + StateFlow
- 01
What is
rememberandrememberSaveablein Compose?Mediumrememberkeeps a value across recompositions, andrememberSaveableadditionally survives configuration changes and process death.@Composable fun Counter() { // remember — survives recomposition, lost on rotation if Activity recreates var count by remember { mutableStateOf(0) } // rememberSaveable — survives rotation + process death (for Parcelable / primitives) … - 02
What is
mutableStateOf,mutableStateListOf,derivedStateOf?MediummutableStateOfmakes a value observable,mutableStateListOfdoes the same for a collection, andderivedStateOfcaches a computation over other state.@Composable fun TodoList() { val items = remember { mutableStateListOf("buy milk", "write tests") } var draft by remember { mutableStateOf("") } Column { … - 03
What is state hoisting? Why is it the recommended pattern?
MediumState hoisting moves state OUT of a composable into its caller.
// Before — stateful, hard to reuse @Composable fun StatefulSearchBar() { var query by remember { mutableStateOf("") } TextField(value = query, onValueChange = { query = it }) } … - 04
How do you connect a ViewModel + StateFlow to Compose UI?
MediumThe ViewModel owns the screen state as a
StateFlow<UiState>and the composable reads it withcollectAsStateWithLifecycle().// State + ViewModel data class HomeUiState(val isLoading: Boolean = false, val items: List<Post> = emptyList(), val error: String? = null) class HomeViewModel(private val repo: PostsRepo) : ViewModel() { private val _state = MutableStateFlow(HomeUiState(isLoading = true)) val state: StateFlow<HomeUiState> = _state.asStateFlow() … - 05
What are LaunchedEffect, DisposableEffect, SideEffect, and produceState?
HardCompose effect handlers exist because a composable body may re-run at any moment, so a side effect needs to start, restart and clean up in step with composition.
// Network call when key changes @Composable fun User(id: String) { var user by remember { mutableStateOf<User?>(null) } LaunchedEffect(id) { // restarts when id changes user = api.fetchUser(id) … - 06
What causes excessive recomposition and how do you minimize it?
HardExcessive recomposition comes from composables Compose cannot skip, and the fix is nearly always to make parameters comparable or to move the state read further down the tree.
// ❌ Read state high in the tree — Header and Body re-run on every keystroke @Composable fun ParentRead(text: String) { Header(); Body(); Text(text) } // ✅ Push the read down to the leaf that needs it @Composable … - 07
A counter composable keeps
var count = 0in its body and doescount++on click, but the number on screen never moves. What is wrong?EasyA plain local variable is re-initialised on every recomposition and, worse, writing to it tells Compose nothing.
@Composable fun BrokenCounter() { var count = 0 // re-initialised on every recomposition Button(onClick = { count++ }) { // and nothing is observing it Text("Clicked " + count) } … - 08
When is a
CompositionLocalthe right way to get a value down the tree, and when is it an abuse of one?MediumA CompositionLocal hands a value implicitly to everything below the provider, which is right for ambient context and wrong for the data a screen is about.
// Changes during the session -> trackable local val LocalDateFormatter = compositionLocalOf { DateFormatter(Locale.getDefault()) } // Never changes, and there is no sensible default -> static, and fail loudly val LocalAnalytics = staticCompositionLocalOf<Analytics> { error("LocalAnalytics not provided") } … - 09
Your screen composable has accumulated eight
remembervalues plus the logic that coordinates them. What do you extract, and does it belong in a ViewModel?MediumSplit the state by who cares about it: UI-only state moves into a plain state holder class remembered in the composition, and anything the rest of the app cares about moves into a ViewModel.
@Stable class CheckoutFormState( initialEmail: String, private val scope: CoroutineScope, val listState: LazyListState ) { … - 10
Typing fast into a
TextFieldwhose value comes back from a ViewModel drops or reorders characters. Why does that happen, and how does the state-based text field API avoid it?MediumThe
valueplusonValueChangetext field is a round trip through your state holder, so if any step of that loop is asynchronous the field can render text older than what the user already typed.// ❌ round trip through an async pipeline — fast typing drops characters @Composable fun BrokenSearch(vm: SearchViewModel) { val state by vm.state.collectAsStateWithLifecycle() // debounced upstream TextField(value = state.query, onValueChange = vm::onQueryChange) } … - 11
What is the snapshot system underneath Compose state, and what does it actually guarantee?
HardCompose state lives in snapshots — versioned, isolated views of memory that give every reader a consistent picture and make a group of writes behave like a transaction.
val name = mutableStateOf("ada") val age = mutableIntStateOf(36) // Batch writes: no reader can observe a half-updated pair Snapshot.withMutableSnapshot { name.value = "grace" … - 12
After inserting a row at the top of a
LazyColumn, the expanded row is suddenly the wrong one. What is going on, and how do you fix it?HardRemembered state is identified by position in the composition, so when items shift the state stays with the slot instead of following the item.
// ❌ no key — inserting at the top shifts every row into its neighbour's slot LazyColumn { items(posts) { post -> PostRow(post) } } // ✅ stable identity: state follows the item, and the move can be animated … - 13
A dialog holds
remember { mutableStateOf(initialName) }, the parent passes a new name, and the field still shows the old one — why?Easyrememberruns its calculation the first time that call site is reached and never again, so a parameter that changes later never gets back into the state it seeded.// ❌ initialName is read once; the parent can never change it afterwards @Composable fun RenameDialog(initialName: String, onSave: (String) -> Unit) { var name by remember { mutableStateOf(initialName) } TextField(value = name, onValueChange = { name = it }) Button(onClick = { onSave(name) }) { Text("Save") } … - 14
A reviewer asks you to rewrite
val scroll = remember { mutableStateOf(0) }withbyandmutableIntStateOf— what do those two edits actually change?Easybyis a property delegate over the very sameMutableStateobject — pure syntax — whilemutableIntStateOfswaps the boxed backing field for a primitive one, so a value written every frame stops allocating.@Composable fun Demo(listState: LazyListState) { // 1. plain — you hold the MutableState and can pass it on val tab = remember { mutableStateOf(0) } Text("tab ${tab.value}") … - 15
Your ViewModel exposes
StateFlow<UiState>and a teammate wantsvar uiState by mutableStateOf(...)instead — what actually differs between them?MediumBoth are legitimate; what really differs is the dependency the ViewModel takes on, whether you get Flow operators, and how fine-grained the resulting recomposition is.
// StateFlow — the common default: operators, no Compose types in the ViewModel class SearchViewModel(repo: Repo) : ViewModel() { private val _query = MutableStateFlow("") val query: StateFlow<String> = _query.asStateFlow() val results: StateFlow<List<Hit>> = _query … - 16
The log shows the new item inside the state, but the list on screen never redraws it — the ViewModel added it with
_state.value.items.add(item). What happened?MediumThe list was mutated in place, so the state value is still the same instance and still equal to itself, and both
StateFlowand Compose state drop an update that compares equal.data class CartUiState(val items: List<Item> = emptyList(), val total: Int = 0) class CartViewModel : ViewModel() { private val _state = MutableStateFlow(CartUiState()) val state: StateFlow<CartUiState> = _state.asStateFlow() … - 17
Each step of a three-screen wizard calls
viewModel(), and step three sees nothing the user entered in step one — why, and how do you share one owner?MediumInside a
NavHost,viewModel()resolves against that destination's ownNavBackStackEntry, so every step gets a separate ViewModel, cleared the moment its entry pops.@Serializable object Wizard @Serializable object Step1 @Serializable object Step2 @Serializable data class Confirm(val orderId: String) NavHost(navController, startDestination = Wizard) { … - 18
Tapping the favourite star fills it, then it empties for a beat before filling again — what is wrong with how that state is owned?
MediumTwo owners are writing the same value: the row keeps a local
remembercopy that flips instantly, and the ViewModel's state arrives a frame later still holding the old value and overwrites it.// ❌ two owners: the local mirror wins for one frame, then the ViewModel overwrites it @Composable fun PostRow(post: Post, onFavourite: (Long) -> Unit) { var favourite by remember { mutableStateOf(post.isFavourite) } IconToggleButton(checked = favourite, onCheckedChange = { favourite = it // instant … - 19
A header that slides as the user scrolls recomposes on every frame — how do you make the scroll offset move it without recomposing anything?
HardRead the scroll value inside a lambda that Compose invokes during layout or draw instead of in the composable body — the phase in which a state is read decides how much work its change costs.
// ❌ read in the composable body: every scrolled pixel recomposes the header @Composable fun CollapsingHeader(listState: LazyListState, title: String) { val offsetPx = listState.firstVisibleItemScrollOffset val offsetDp = with(LocalDensity.current) { -offsetPx.toDp() } Box(Modifier.offset(y = offsetDp)) { Text(title) } … - 20
A user fills your form, takes a twenty-minute call, comes back and the screen is blank — which of your state should have survived that, and how do you test it?
HardThe process was killed in the background and the Activity was rebuilt from its saved instance state, which the ViewModel is not part of — only
rememberSaveableandSavedStateHandlecross that line.class EditorViewModel @Inject constructor( private val handle: SavedStateHandle, repo: NoteRepo ) : ViewModel() { private val args = handle.toRoute<Editor>() // nav argument, restored for free …