MVVM vs MVI

Architecture and dependency injection Mid-level Kotlin 2.0+ API 24+ Reviewed By Best Latest Apps

Pick the fix

A team's screens have many MutableStateFlows that drift out of sync. They want predictable state without much ceremony. Best step?

Options
Hint 1 of 1

What causes the drift?

Choose an answer, then open Show answer below to check it.

Show answer

Answer: A Merge them into one immutable UiState updated with update { }

Correct. One state and one owner fix the drift; full MVI is optional.

Why not the others?
  • B Add an event bus between ViewModels. More moving parts, same drift.
  • C Switch to LiveData. Changes the stream type, not the design.
  • D Move state into the composables. Spreads state further.

30-second answer

Both keep logic out of the UI. In MVVM, the ViewModel exposes state and functions the UI calls. In MVI, the UI sends intents (events) as objects to a single entry point, a reducer turns the current state and the intent into a new state, and side effects are handled separately. MVI makes every change explicit and replayable; MVVM with one UiState is lighter and is what most Android teams use.

Practise saying it · 35 s. No recording, just a timer

Compare MVVM and MVI.

Say your answer out loud, then check which of these points you covered:

  • Both keep logic out of UI
  • MVI: intents, reducer, single state
  • MVVM: functions and state
  • Single UiState narrows the gap

Deep dive

Many 'MVVM' apps with a single UiState and UDF are already close to MVI.

MVI's sealed intent type documents every action a screen supports.

A pure reducer (State, Intent) -> State is trivial to unit test.

MVI can add boilerplate on simple screens.

A tiny reducer Compile-checked · Kotlin 2.4.21
data class CounterState(val count: Int = 0)

sealed interface CounterIntent {
    data object Increment : CounterIntent
    data class Set(val value: Int) : CounterIntent
}

fun reduce(state: CounterState, intent: CounterIntent): CounterState = when (intent) {
    CounterIntent.Increment -> state.copy(count = state.count + 1)
    is CounterIntent.Set -> state.copy(count = intent.value)
}

Common mistakes

  • Treating them as completely different architectures.
  • Picking MVI and then mutating state outside the reducer.
  • Using MVI with many separate state flows.

Senior take & follow-ups

Say they share UDF, then name the real difference: an explicit intent type and a reducer.

Follow-up questions

Think of your answer first, then open the hint.

  1. What's a reducer?

    Show hint

    A pure function from the current state and an event to the next state.

  2. Where do side effects like navigation go in MVI?

    Show hint

    A separate effect channel or state flag, outside the pure reducer.

Similar on iOS: MVVM done right The iOS page covers MVVM with Observation; MVI is more common on Android.

Suggest a fix if something here is wrong or out of date.