MVI Pattern Quiz
ARCHITECTURE › Patterns
What best describes the data flow in MVI?
- State flows to the View, and intents flow up into new state
- The View mutates shared state directly and then notifies the Model
- Two-way binding keeps View and Model fields synchronized automatically
- The Model pushes partial updates straight into View widgets as they happen
Answer: State flows to the View, and intents flow up into new state
MVI is unidirectional: immutable state flows down to render the View, while user intents flow up and are folded into a new state by the reducer.
Which signature correctly captures an MVI reducer?
- (intent) -> Unit, mutating the existing state object in place
- (state, intent) -> newState, a pure function returning new state
- (state) -> Flow<state>, subscribing to the data layer stream directly
- (view, state) -> Unit, binding state fields onto the View
Answer: (state, intent) -> newState, a pure function returning new state
A reducer is a pure function of the current state and an intent/result that returns a new immutable state; it performs no side effects, which makes it deterministic and easy to test.
Why is a single immutable state object central to MVI's debuggability?
- It lets multiple components each own and mutate a separate slice of state
- It lets the View patch fields directly, so no reducer is needed at all
- It removes the need for a ViewModel, since state lives only in the View
- Each transition produces a new snapshot you can log, diff, and replay
Answer: Each transition produces a new snapshot you can log, diff, and replay
Because every transition produces a new immutable snapshot from a deterministic reducer, you can record and replay the exact sequence of states, which is what enables time-travel debugging.
How should a network call's result be incorporated in MVI?
- Run the call inside the reducer so the new state returns synchronously
- Let the View write the response straight into the state object itself
- Do the call as a side effect, then reduce its result into state
- Bypass state and update UI widgets directly from the data layer callback
Answer: Do the call as a side effect, then reduce its result into state
Reducers must stay pure, so async work happens outside them; the result re-enters the loop as an intent/result that the reducer reduces into the next immutable state.
When is MVI's extra boilerplate most justified over plain MVVM?
- On complex screens where explicit, replayable transitions cut bugs
- On a static screen that only displays a fixed title and an icon
- Whenever a screen has just a single button and no async work at all
- Only when the app has no ViewModels defined anywhere at all
Answer: On complex screens where explicit, replayable transitions cut bugs
The state class, intent hierarchy, reducer, and effect plumbing pay off when state complexity is high; for trivial screens the ceremony outweighs the benefit.
Per Android's architecture recommendations, how should one-off events such as showing a snackbar be handled?
- Expose a separate SharedFlow of UI commands the ViewModel emits
- Handle them in the ViewModel and update the exposed uiState
- Throw an exception and let the View render the snackbar itself
- Store them in a global singleton and have the UI poll it each frame
Answer: Handle them in the ViewModel and update the exposed uiState
The guidance is to avoid pushing events from the ViewModel to the UI; instead handle them in the ViewModel and update the single uiState, preserving one source of truth across lifecycle changes.
What is a key structural difference between MVI and MVVM?
- MVI does not ban ViewModels, while MVVM commonly uses them
- MVVM usually exposes mutable fields, while MVI keeps state immutable
- MVI routes input through one intent channel and one immutable state
- MVI removes the data layer, whereas MVVM still keeps one around
Answer: MVI routes input through one intent channel and one immutable state
MVI constrains interaction to a single intent channel and one immutable state object, making transitions explicit; MVVM commonly exposes several observable properties and lets the View call arbitrary methods. MVI is often implemented on top of a ViewModel.
In idiomatic Kotlin, what is the best way to model the set of intents a screen accepts?
- An enum class, on the basis that intents never carry parameters
- A bag of String constants dispatched by a when on the string value
- A sealed interface or sealed class, giving compile-time when checks
- Unrelated top-level functions, one per action, sharing no common supertype
Answer: A sealed interface or sealed class, giving compile-time when checks
A sealed interface/class lets intents carry their own payloads and makes a when expression exhaustive at compile time, so adding a new intent forces every handler to address it.
Given an immutable UiState data class, how do you produce the next state that changes just one field?
- Call copy() on the current state and override one field
- Flip the field through a var and re-emit the same object
- Create a new MutableStateFlow for each field update
- Keep one LiveData per field so copying the state is unnecessary
Answer: Call copy() on the current state and override one field
data class copy() returns a fresh immutable instance with the requested field overridden and all others carried over, which is exactly how a reducer builds the next state without mutation.
In a Compose MVI screen, how is the state typically exposed and consumed?
- Expose a public MutableStateFlow and read its .value once during onCreate
- Pass the ViewModel into each composable so it can read individual fields directly
- Keep the state in a global object that composables read without collecting
- Collect UiState with collectAsStateWithLifecycle; pass onIntent down
Answer: Collect UiState with collectAsStateWithLifecycle; pass onIntent down
Collecting the StateFlow with collectAsStateWithLifecycle gives lifecycle-aware state-down rendering, while an onIntent lambda carries events up, realizing the MVI loop natively in Compose.
For a true one-off effect like a single navigation command, why is a Channel (consumed as a Flow) often preferred over a StateFlow?
- A Channel buffers every effect forever, guaranteeing none are ever dropped
- A StateFlow replays its latest value, so nav re-fires; Channel fires once
- A StateFlow cannot carry non-primitive payloads such as a route object
- A Channel automatically survives full process death while a StateFlow doesn't
Answer: A StateFlow replays its latest value, so nav re-fires; Channel fires once
StateFlow is a hot, value-conflating holder that replays its current value to every new collector, which would re-trigger a one-shot navigation on re-collection; a Channel hands each emitted effect to a single consumer once.
Which operation placed inside a reducer would break its purity?
- Returning state.copy(count = state.count + 1) after each intent
- Using when on the incoming intent to pick the next state
- Reading System.currentTimeMillis() to timestamp the new state
- Computing a boolean from values already stored in the current state
Answer: Reading System.currentTimeMillis() to timestamp the new state
A pure reducer must depend only on its inputs; reading the wall clock introduces nondeterminism, so the timestamp should be captured outside and passed in as part of the intent/result.
An MVI ViewModel reduces a result but the new UiState is structurally equal to the previous one. What keeps the UI from doing redundant work?
- Compose always recomposes on every emission, even if values match
- StateFlow uses equals, and Compose skips unchanged inputs
- The reducer must manually compare states and drop equal ones itself
- collectAsStateWithLifecycle randomly drops emissions to save work
Answer: StateFlow uses equals, and Compose skips unchanged inputs
StateFlow conflates and compares values with equals, so an equal state is never re-emitted, and Compose's stability checks then skip recomposing composables whose inputs did not change.
A screen must display data from both a user repository and a settings flow. The MVI-idiomatic approach is to:
- combine the flows and fold them into one immutable UiState StateFlow
- expose one StateFlow per source and merge them in each composable body
- collect both flows in the composable and keep the merged value in remember
- put each source in its own ViewModel and bind them to the View separately
Answer: combine the flows and fold them into one immutable UiState StateFlow
MVI keeps a single source of truth, so multiple inputs are combined and folded into one immutable UiState in the ViewModel rather than letting the View reconcile several independent states.