Compose State & Hoisting Quiz
UI › Compose Core
A composable has var name = mutableStateOf("") (no remember) and a TextField bound to it. The user types but nothing appears. What is the primary problem?
- TextField requires rememberSaveable state rather than plain mutableStateOf
- mutableStateOf may only be used inside a ViewModel, never within a composable
- A new MutableState is created each recomposition without remember, losing it
- The State must be collected with collectAsState() before it can be read out
Answer: A new MutableState is created each recomposition without remember, losing it
On each recomposition the un-remembered mutableStateOf re-initializes to "", so the update is lost. Wrapping it in remember keeps the same state instance across recompositions.
Which statement about remember vs rememberSaveable is correct?
- rememberSaveable survives rotation/config changes; remember is lost
- remember persists across process death, but rememberSaveable doesn't
- Both are cleared on every recomposition, so neither keeps state
- rememberSaveable stores any object, even non-Parcelable ones, with no Saver
Answer: rememberSaveable survives rotation/config changes; remember is lost
rememberSaveable uses the saved instance state mechanism to survive activity/config recreation; plain remember is lost. Custom non-Bundleable types still need a Saver or @Parcelize.
You are converting a stateful composable into a stateless one. Which signature best expresses correct state hoisting?
- fun Field(state: MutableState<String>, enabled: Boolean = true)
- fun Field(viewModel: FieldViewModel, modifier: Modifier = Modifier)
- fun Field(initial: String = ""): String
- fun Field(value: String, onValueChange: (String) -> Unit)
Answer: fun Field(value: String, onValueChange: (String) -> Unit)
Hoisting passes the immutable value down and an event lambda up (value + onValueChange), keeping the composable stateless, reusable, and giving the caller a single source of truth.
In a chat screen, showScrollToTop should be true only when the list is scrolled past the first item. The scroll offset updates on every frame while scrolling. What is the most efficient way to drive the button's visibility?
- Read listState.firstVisibleItemScrollOffset directly in the button composable
- Wrap the boolean in derivedStateOf { listState.firstVisibleItemIndex > 0 }
- Store the boolean in rememberSaveable and update it in a LaunchedEffect each frame
- Recompute it in a ViewModel StateFlow on every scroll event
Answer: Wrap the boolean in derivedStateOf { listState.firstVisibleItemIndex > 0 }
derivedStateOf recomputes from the rapidly changing input but only notifies readers when the derived boolean actually flips, avoiding recomposition on every frame of scrolling.
Following the official rules, where should a piece of state be hoisted?
- Always up to the very root composable of the entire app, for global consistency
- Always into a ViewModel, regardless of the state's actual scope or lifetime
- To at least the lowest common parent that reads it and highest that changes it
- To whichever composable happened to render first during the initial composition
Answer: To at least the lowest common parent that reads it and highest that changes it
The guidance is to hoist to at least the lowest common parent that reads the state and at least the highest level that changes it, and to hoist states that change on the same events together.
Which approach correctly uses a non-observable collection as Compose state?
- Use mutableStateListOf() or an immutable List in MutableState
- Use a plain ArrayList and call recompose() after each mutation
- Use mutableListOf() and add items directly; Compose tracks them
- Use a var ArrayList with @Stable so mutations become observable
Answer: Use mutableStateListOf() or an immutable List in MutableState
Plain ArrayList/mutableListOf are not observable, so mutations do not schedule recomposition. mutableStateListOf, or replacing an immutable List held in MutableState, is observable.
What is the recommended practice for providing a ViewModel's state to a deep child composable?
- Expose the ViewModel as a CompositionLocal so any child depth can read it
- Pass the ViewModel instance down through each intermediate composable by hand
- Store the state in a global singleton object that the child reads directly
- Obtain the ViewModel only at screen level, passing state and lambdas down
Answer: Obtain the ViewModel only at screen level, passing state and lambdas down
ViewModels should be obtained at screen level only; children receive hoisted immutable state and callbacks, keeping them stateless, reusable, and testable rather than coupled to the ViewModel.
To avoid writing .value everywhere you declare var text by remember { mutableStateOf("") } and access text directly. What actually makes this delegated property work while staying observable?
- A dedicated compiler intrinsic that only handles State declared at file top level
- getValue/setValue operator extensions on (Mutable)State back the delegate
- by silently re-wraps the value in rememberSaveable so it can be delegated
- by unwraps the State into a plain var, trading away observability for convenience
Answer: getValue/setValue operator extensions on (Mutable)State back the delegate
Compose ships getValue/setValue operator extensions for State and MutableState; importing them lets the by delegate read and write through .value while the value remains observable State.
You write remember { expensiveParse(json) }, but when the json parameter changes the composable keeps showing the previously parsed result. What is the correct fix?
- Switch to rememberSaveable so it reparses the input whenever it changes
- Move the parse into LaunchedEffect(Unit) so it runs exactly once
- Pass json as a key: remember(json) { expensiveParse(json) }
- Wrap the call in derivedStateOf so the parse is forced to recompute
Answer: Pass json as a key: remember(json) { expensiveParse(json) }
remember caches its result across recompositions and only recomputes when one of its key arguments changes by equals; passing json as a key invalidates the cache whenever json changes.
rememberSaveable { mutableStateOf(myFilter) }, where MyFilter is an ordinary data class, throws at runtime. Why, and what is the cleanest fix?
- rememberSaveable cannot hold MutableState; drop the mutableStateOf wrapper
- Saveable values must be primitives, so convert the filter to JSON yourself
- Annotate MyFilter with @Stable so rememberSaveable can serialize it
- Only Bundle-storable types work; make MyFilter Parcelable or use a Saver
Answer: Only Bundle-storable types work; make MyFilter Parcelable or use a Saver
rememberSaveable persists through saved instance state, which is Bundle-backed, so an arbitrary class fails unless it is Bundleable; making it @Parcelize or passing a Saver resolves it.
Inside a LaunchedEffect you want to fire a suspend analytics call only when listState.firstVisibleItemIndex actually changes. Which API best bridges Compose State to a Flow you can collect there?
- snapshotFlow { listState.firstVisibleItemIndex } in the effect
- listState.firstVisibleItemIndex.collectAsState() inside the effect
- derivedStateOf { listState.firstVisibleItemIndex }.asFlow() here
- mutableStateOf(listState.firstVisibleItemIndex).asStateFlow() now
Answer: snapshotFlow { listState.firstVisibleItemIndex } in the effect
snapshotFlow turns reads of Compose State into a cold Flow that emits a new value whenever the observed snapshot state changes, which is exactly how you drive suspend work from inside an effect.
Which of these is the WEAKEST candidate for derivedStateOf (least likely to provide any benefit)?
- showButton computed from a scroll offset that updates every frame
- fullName produced by concatenating firstName and lastName, both edited directly in TextFields
- isFormValid computed from many fields where the validity result rarely flips
- a sorted, filtered list recomputed from a frequently updated source whose visible result rarely changes
Answer: fullName produced by concatenating firstName and lastName, both edited directly in TextFields
derivedStateOf only pays off when its inputs change far more often than the computed result; concatenating two names changes the output on every input change, so it adds overhead without reducing recompositions.
With var count by remember { mutableStateOf(0) }, you execute count = 0 while it already holds 0. What happens?
- Recomposition is always scheduled, because any State assignment counts.
- It throws, because writing the same value back to State is forbidden.
- No recomposition is scheduled: equal values are treated as unchanged.
- The whole Composition invalidates and recomposes from the root every time.
Answer: No recomposition is scheduled: equal values are treated as unchanged.
mutableStateOf defaults to structuralEqualityPolicy, so writing a value that equals (by equals) the current one is a no-op that does not invalidate readers; a policy like neverEqualPolicy would instead force notification.
A reusable stateless SearchField(value, onValueChange) also keeps var internal by remember { mutableStateOf(value) } and displays internal instead of value. Users report it ignores programmatic updates pushed from the parent. Which principle is violated?
- Recomposition scoping; wrap the field in a unique key() for updates
- Persistence; the internal value should have been stored with rememberSaveable
- Nothing is wrong; this is the standard controlled-component pattern
- Single source of truth; internal state can diverge from the hoisted value
Answer: Single source of truth; internal state can diverge from the hoisted value
A hoisted, controlled composable must render the passed-in value directly; duplicating it in internal remember state creates a second source of truth that ignores parent updates and breaks unidirectional data flow.