Show how copy() underpins immutable state transitions in a reducer-style update. Why is this preferable to in-place mutation, especially for concurrency and frameworks like Jetpack Compose / StateFlow?
answer
- Reducer: (State, Action) -> State via copy()
- StateFlow.update { it.copy(...) } = CAS, thread-safe
- Immutable = tear-free atomic publication
- Structural equals drives emit/recompose skipping
- Shallow copy + allocation cost are the caveats
basics
~20 sYou model state as an immutable data class and produce each new state with copy(). Because every state is a distinct, unchanging object, comparing old vs new is reliable, change detection works, and concurrent readers never see a half-updated object.
solid answer
~50 sA reducer takes the current immutable state plus an action and returns a NEW state via copy(), e.g. state.copy(loading = false, items = result). Since data classes are immutable values, each transition yields a distinct object you can publish atomically — ideal for StateFlow.update { it.copy(...) }, which applies the change with compareAndSet and is safe under concurrency. Immutability gives reliable structural equals(): Compose and Flow can skip recomposition/emission when the new state equals the old, and detect change by identity/equality rather than guessing. In-place mutation breaks this: observers may see torn reads, equals() can't tell old from new (same reference), and shared mutable state needs locking. Caveats: copy() is shallow, so nested mutable types must also be immutable; and rapid copying of large states has allocation cost, mitigated by structural sharing of unchanged branches.
code
kotlin · 8 linesdata class UiState(val loading: Boolean = false, val items: List<String> = emptyList())
val state = MutableStateFlow(UiState())
// thread-safe, immutable transition
state.update { it.copy(loading = true) }
// ...later
state.update { it.copy(loading = false, items = listOf("a", "b")) }go deeper
Can write state.copy(...) to change a field but may not connect it to concurrency or frameworks.
Uses copy() in reducers and StateFlow.update and knows it avoids in-place mutation.
Explains atomic publication, CAS-based update, equality-driven skipping, and the shallow-copy/allocation caveats.
Sets state-management architecture: immutability boundaries, structural sharing, Compose stability, and trade-offs at scale.
## The pattern Model application state as an **immutable data class** and compute every new state from the old one with `copy()`. A **reducer** is a pure function `(State, Action) -> State`: ```kotlin data class UiState( val loading: Boolean = false, val items: List<String> = emptyList(), val error: String? = null, ) fun reduce(state: UiState, action: Action): UiState = when (action) { Action.Load -> state.copy(loading = true, error = null) is Action.Loaded -> state.copy(loading = false, items = action.items) is Action.Failed -> state.copy(loading = false, error = action.message) } ``` Each branch returns a **brand-new** `UiState`; the previous one is never modified. ## Why immutable transitions beat in-place mutation ### 1. Atomic, tear-free publication With immutability, a reader either sees the old complete object or the new complete object — never a half-updated one. In-place mutation can expose **torn reads** where some fields are updated and others aren't. ### 2. Concurrency safety with StateFlow `MutableStateFlow` exposes `update { ... }`, which reads the current value, applies your transform, and stores it with **compareAndSet** (a CAS loop). Used with `copy()` it is the idiomatic thread-safe transition: ```kotlin val state = MutableStateFlow(UiState()) state.update { it.copy(loading = true) } // safe under concurrent updates ``` Because each value is immutable, no extra locking is needed to read it. ### 3. Reliable change detection / equality Data classes generate structural `equals()`. `StateFlow` only emits when the new value is **not equal** to the current one, and **Jetpack Compose** skips recomposition when inputs are unchanged. A fresh `copy()` that differs triggers updates; an equal value is deduplicated. With in-place mutation the reference is unchanged, so frameworks can't tell something changed (or conversely emit spuriously). ### 4. Time-travel / debuggability Keeping past immutable states enables undo, logging, and reproducing bugs — impossible if state is mutated away. ## Caveats a senior should name - **Shallow copy**: nested mutable types reintroduce sharing bugs; keep the whole tree immutable. - **Allocation cost**: every transition allocates; for large/deep states use structural sharing (unchanged branches are reused) and avoid copying in hot loops. - **Stability for Compose**: immutable types help Compose treat parameters as stable, improving skipping. ## Summary copy() turns state changes into the production of new immutable values, which is what makes CAS-based `StateFlow.update`, Compose recomposition, and concurrent reads correct and predictable.
- Why is StateFlow.update { it.copy(...) } safer than reading value, copying, then assigning value?update uses compareAndSet in a retry loop, so it won't clobber a concurrent change; a naive read-modify-write can lose updates under contention.
- When does StateFlow NOT emit after an update?When the new value equals() the current one — StateFlow deduplicates equal values, which data-class structural equality makes reliable.
- What is the main performance concern of copy()-based state?Allocation per transition; mitigated by structural sharing of unchanged branches and avoiding copies in tight loops.
Each state is a numbered photograph: you snap a new photo for every change instead of scribbling over the last one, so you can always compare frames and never catch a half-edited shot.
saying these in an interview costs you the question
- Mutating MutableStateFlow's value object in place
- Doing value = value.copy(...) instead of update { } and ignoring lost-update races
- Forgetting shallow-copy nested mutability undermines the model
- Claiming immutability has no allocation cost
- Not connecting structural equals() to emit/recompose skipping