Introduction
State management is one of the most critical challenges in Android development. Here at zen8labs, my colleagues and I adopt the Model-View-Intent (MVI) architecture to keep our applications scalable, maintainable, and predictable. One of the core components of MVI is the Reducer, responsible for transforming events into state updates in a pure and predictable manner. In my blog for you, we will take a deep dive into:
- What is an MVI?
- Understanding Reducers in MVI
- Implementing MVI with Jetpack Compose (Example: Counter App)
By the end of this tutorial, you will have a fully functional Counter App, where users can increment, decrement, and reset a counter using MVI with Jetpack Compose.
What is an MVI?
MVI is a unidirectional state management pattern where:
- User actions trigger events.
- Reducers process events and update the state.
- Side Effects handles API calls, database operations, and UI interactions.
Why use MVI?
- Predictability – The state is updated in a controlled way.
- Testability – Since reducers are pure functions, they can be easily tested.
- Maintainability – UI logic and business logic are separated.

Understanding reducers in MVI
A Reducer is a pure function that:
- Takes the current state and events.
- Returns to a new, immutable state.
- Optionally triggers side effects.
A Reducer does not:
- Modify existing state directly.
- Perform network or database operations.
- Interact with UI components.
By keeping reducers pure and predictable, debugging becomes easier, and testability is improved.
Example: Counter App using MVI with jetpack compose
We will build a Counter App where users can:
- Increment the Counter
- Decrement the Counter
- Reset the Counter
1. Define the ViewState
The CounterViewState holds the current state of the counter.
@Immutable
data class CounterViewState(
val count: Int = 0
)
2. Define events
Each user action in MVI is represented as an event.
@Immutable
sealed class CounterEvent {
data object Increment : CounterEvent()
data object Decrement : CounterEvent()
data object Reset : CounterEvent()
}
3. Define Side Effects
Side effects are one-time actions like showing a Snackbar.
Side effects are one-time actions like showing a Snackbar.
@Immutable
sealed class CounterSideEffect {
data class ShowErrorPopup(val message: String) : CounterSideEffect()
}
Why use SideEffects?
- They separate UI interactions from state updates.
- This keeps the Reducer pure and testable.
4. Implement the Reducer
A Reducer takes the current state and an event, then returns a new state.
CounterReducer {
fun reduce(
previousState: CounterViewState,
event: CounterEvent
): Pair<CounterViewState, CounterSideEffect?> {
return when (event) {
is CounterEvent.Increment -> {
previousState.copy(count = previousState.count + 1) to CounterSideEffect.LogEvent("Incremented to ${previousState.count + 1}")
}
is CounterEvent.Decrement -> {
if (previousState.count > 0) {
previousState.copy(count = previousState.count - 1) to CounterSideEffect.LogEvent("Decremented to ${previousState.count - 1}")
} else {
previousState to CounterSideEffect.ShowErrorPopup("Count cannot be negative")
}
}
is CounterEvent.Reset -> {
CounterViewState(count = 0) to CounterSideEffect.LogEvent("Counter reset to 0")
}
is CounterEvent.Error -> {
previousState.copy() to CounterSideEffect.ShowErrorPopup(event.message)
}
}
}
}
5. Implement the ViewModel
The ViewModel acts as the bridge between UI and Reducer.
class CounterViewModel : ViewModel() {
private val reducer = CounterReducer()
private val _state = MutableStateFlow(CounterViewState())
val state = _state.asStateFlow()
private val _sideEffect = Channel<CounterSideEffect>(Channel.BUFFERED)
val sideEffect = _sideEffect.receiveAsFlow()
fun sendEvent(event: CounterEvent) {
val (newState, effect) = reducer.reduce(_state.value, event)
_state.value = newState
effect?.let { sendSideEffect(it) }
}
private fun sendSideEffect(effect: CounterSideEffect) {
_sideEffect.trySend(effect).onFailure {
println("⚠️ Failed to send SideEffect: ${it?.message}")
}
}
override fun onCleared() {
super.onCleared()
_sideEffect.close()
}}
Why should you use MutableStateFlow
for State? – It ensures real-time UI updates when the state changes.
Why should you use Channel
for SideEffects? – SideEffects should only be received once, and Channel ensures that.
6. Implement the Jetpack Compose UI
Finally, we have created a Composable UI that listens to ViewModel state and side effects.
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CounterScreen() {
val snackBarHostState = remember { SnackbarHostState() }
val state by viewModel.state.collectAsState()
LaunchedEffect(Unit) {
viewModel.sideEffect.collect { effect ->
when (effect) {
is CounterSideEffect.ShowErrorPopup -> {
snackBarHostState.showSnackbar(effect.message)
}
}
}
}
Scaffold(
topBar = {
TopAppBar(title = { Text("Counter App") })
},
snackbarHost = { SnackbarHost(snackBarHostState) }
) { padding ->
Box(
Modifier
.fillMaxSize()
.padding(padding)
) {
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(text = "Count: ${state.count}", fontSize = 24.sp)
Spacer(modifier = Modifier.height(16.dp))
Row {
Button(onClick = { viewModel.sendEvent(CounterEvent.Increment) }) {
Text("Increment")
}
Spacer(modifier = Modifier.width(16.dp))
Button(onClick = { viewModel.sendEvent(CounterEvent.Decrement) },) {
Text("Decrement")
}
Spacer(modifier = Modifier.width(16.dp))
Button(onClick = { viewModel.sendEvent(CounterEvent.Reset) }) {
Text("Reset")
}
}
}
}
}
}
Conclusion
Leveraging MVI architecture to build scalable, maintainable, and predictable Android applications is something that we do daily at zen8labs. By embracing unidirectional data flow, we ensure that our apps remain robust and testable while keeping the UI and business logic neatly separated.
With MVI, you now have a powerful and structured way to handle state in Jetpack Compose. Whether you’re working on a simple counter app or a complex enterprise project, this approach scales effortlessly. However if your next project requires the best in the business to help you, then ask us here, together we can create an awesome project together!
Duc Duong – Mobile Engineer