Mobile Development 16 min read

Choosing the Right MVI Architecture for Android Compose: Pure, Reducer, or State Machine

This article compares three Model‑View‑Intent (MVI) variants—Pure MVI, MVI with a Reducer, and MVI with a State Machine—detailing their core concepts, code examples, advantages, disadvantages, and when each is best suited for Android Jetpack Compose projects.

AndroidPub
AndroidPub
AndroidPub
Choosing the Right MVI Architecture for Android Compose: Pure, Reducer, or State Machine

In Android development, especially with Jetpack Compose, the Model‑View‑Intent (MVI) pattern provides a single source of truth for the UI, enhancing predictability, testability, and debuggability.

1. Pure MVI: Simple Direct Implementation

Pure MVI is the most straightforward form where user Intent maps directly to UI State changes, all handled inside a ViewModel without additional abstraction layers.

Code Example (Counter Scenario)

// State: immutable data class representing UI state
// Architecture feature 1: State immutability → ensures traceable changes

data class CounterState(val count: Int = 0)

// Intent: sealed class enumerating user actions
// Architecture feature 2: exhaustive Intent list → covers all possible behaviors
sealed class CounterIntent {
    object Increment : CounterIntent() // increase count
    object Decrement : CounterIntent() // decrease count
}

// ViewModel: processes Intent and produces new State
// Architecture feature 3: centralized logic → no extra layers, direct binding
class CounterViewModel : ViewModel() {
    private val _state = MutableStateFlow(CounterState())
    val state: StateFlow<CounterState> = _state // expose immutable flow

    fun handleIntent(intent: CounterIntent) {
        // Architecture feature 4: direct mapping of Intent to State
        when (intent) {
            CounterIntent.Increment -> _state.value = _state.value.copy(count = _state.value.count + 1)
            CounterIntent.Decrement -> _state.value = _state.value.copy(count = _state.value.count - 1)
        }
    }
}

Core Architectural Features

No abstraction layer, logic centralized : All Intent handling and State transformation reside in the ViewModel, making the code intuitive.

State immutability + single flow : Using a data class copy ensures each State change is traceable, and StateFlow guarantees the UI receives the latest single source.

Strong Intent‑State binding : Each Intent directly updates the State within handleIntent, enabling quick location of logic.

Advantages

Minimal code : No extra layers like Reducer or state‑machine classes; a few lines achieve functionality, speeding up development.

Intuitive logic : Direct mapping of Intent to State makes the flow easy for newcomers.

Zero learning cost : No need to grasp pure functions or state‑machine concepts, suitable for small teams or simple projects.

Disadvantages

Poor maintainability : As business complexity grows, the ViewModel becomes cluttered with many when branches and transformation logic.

Average testability : StateFlow ties logic to ViewModel, requiring mocks for testing instead of pure function verification.

Lack of state protection : Without constraints, illegal state transitions can occur, leading to UI anomalies.

2. MVI with Reducer: Pure Function Testability

The Reducer pattern extracts the "State + Intent → New State" conversion into a pure function, separating it from the ViewModel and improving testability and code cleanliness.

Code Example (Counter Scenario)

// Reuse CounterState and CounterIntent from Pure MVI

// Reducer function: pure, side‑effect‑free transformation
fun reduce(state: CounterState, intent: CounterIntent): CounterState =
    when (intent) {
        CounterIntent.Increment -> state.copy(count = state.count + 1)
        CounterIntent.Decrement -> state.copy(count = state.count - 1)
    }

// ViewModel: delegates state transformation to Reducer
class CounterReducerViewModel : ViewModel() {
    private val _state = MutableStateFlow(CounterState())
    val state: StateFlow<CounterState> = _state

    fun dispatch(intent: CounterIntent) {
        // Architecture feature: ViewModel only forwards intent
        _state.value = reduce(_state.value, intent)
    }
}

Core Architectural Features

Layered logic, single responsibility : ViewModel handles flow control, Reducer handles pure computation, aligning with the Single Responsibility Principle.

Pure functions boost testability : Reducer has no side effects, allowing isolated unit tests without mocking external dependencies.

Reusable state transformation : Multiple ViewModels can share the same Reducer, reducing duplicate code.

Advantages

Strong testability : Reducer can be tested by feeding State and Intent directly and asserting the output.

Clean code : ViewModel responsibilities shrink to intent forwarding, making maintenance easier.

Clear logic separation : Flow control stays in ViewModel while pure calculations reside in Reducer.

Disadvantages

Still no state protection : Reducer ensures correct input‑output mapping but cannot enforce legal state transitions.

Higher learning curve : Requires understanding of pure functions and the Reducer concept.

Not suited for very complex flows : Multi‑step processes may need more than a simple Reducer.

3. MVI with State Machine: Complex Flow Control

The Finite State Machine (FSM) adds explicit allowed state‑transition rules to MVI, ensuring the UI remains in valid states and ignoring illegal Intents, ideal for multi‑step, tightly coupled workflows.

Code Example (E‑commerce Checkout Flow)

// State: sealed class enumerating all possible checkout states
sealed class CheckoutState {
    object Idle : CheckoutState()               // initial state
    object CollectingAddress : CheckoutState()   // address entry
    object CollectingPayment : CheckoutState()   // payment entry
    object ProcessingPayment : CheckoutState()   // payment in progress
    object Success : CheckoutState()             // payment succeeded
    data class Error(val message: String) : CheckoutState() // error with message
}

// Intent: sealed class enumerating all possible user actions
sealed class CheckoutIntent {
    object StartCheckout : CheckoutIntent()
    data class EnterAddress(val address: String) : CheckoutIntent()
    data class EnterPayment(val card: String) : CheckoutIntent()
    object ConfirmPayment : CheckoutIntent()
    object Retry : CheckoutIntent()
}

// ViewModel: handles state transitions based on current state and intent
class CheckoutViewModel : ViewModel() {
    private val _state = MutableStateFlow<CheckoutState>(CheckoutState.Idle)
    val state: StateFlow<CheckoutState> = _state

    fun handleIntent(intent: CheckoutIntent) {
        when (val current = _state.value) {
            CheckoutState.Idle -> when (intent) {
                is CheckoutIntent.StartCheckout -> _state.value = CheckoutState.CollectingAddress
                else -> Unit
            }
            CheckoutState.CollectingAddress -> when (intent) {
                is CheckoutIntent.EnterAddress -> _state.value = CheckoutState.CollectingPayment
                else -> Unit
            }
            CheckoutState.CollectingPayment -> when (intent) {
                is CheckoutIntent.EnterPayment -> {
                    _state.value = CheckoutState.ProcessingPayment
                    processPayment(intent.card)
                }
                else -> Unit
            }
            CheckoutState.ProcessingPayment -> when (intent) {
                is CheckoutIntent.Retry -> _state.value = CheckoutState.CollectingPayment
                else -> Unit
            }
            else -> Unit // Success or Error states: no handling here
        }
    }

    // Separate side‑effect: network request simulation
    private fun processPayment(card: String) {
        viewModelScope.launch {
            delay(2000) // simulate network delay
            if (card.startsWith("4")) {
                _state.value = CheckoutState.Success // Visa simulated success
            } else {
                _state.value = CheckoutState.Error("Payment failed")
            }
        }
    }
}

Core Architectural Features

Strong state legality enforcement : Combination of current state and Intent guarantees UI never reaches illegal states.

Visualizable flow rules : All transition logic resides in nested when blocks, making the state diagram clear.

Side‑effect isolation : Network or database operations are encapsulated separately, preserving pure state‑transition logic.

Advantages

Absolute state safety : Illegal transitions are impossible, preventing UI anomalies.

Complex flow clarity : Multi‑step business processes are centralized in handleIntent, easing review and debugging.

Debug‑friendly : Precise mapping of state and Intent helps quickly locate bugs.

Side‑effect isolation : Core logic remains pure while side effects are handled elsewhere.

Disadvantages

Largest code footprint : Requires many State and Intent definitions and extensive when branches.

High learning cost : Understanding FSM concepts and transition graphs can be challenging for newcomers.

High maintenance cost : Changes in business flow demand updates to State, Intent, and transition rules.

Overkill for simple scenarios : Using a state machine for trivial features adds unnecessary complexity.

Selection Recommendations

If you need a quick implementation for simple features with no future complexity, choose Pure MVI for minimal code.

For moderately complex functionality where test coverage and code cleanliness matter, opt for Reducer MVI to separate concerns.

When dealing with multi‑step, tightly coupled processes (e.g., checkout, authentication) that require strict state safety, adopt State Machine MVI to enforce flow rules.

Overall, MVI is not a one‑size‑fits‑all architecture; start with the simplest mode and progressively introduce Reducer or State Machine abstractions as the application’s complexity grows.

ArchitectureAndroidState ManagementKotlinJetpack ComposeMVI
AndroidPub
Written by

AndroidPub

Senior Android Developer & Interviewer, regularly sharing original tech articles, learning resources, and practical interview guides. Welcome to follow and contribute!

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.