MVVM vs MVI in Jetpack Compose – Which One Should You Use?
Jetpack Compose has completely changed how we build Android UIs. No more XML. Just pure Kotlin code to declare your UI. But with great UI power comes great architecture confusion!
The big question:
❓ Should you use MVVM or MVI with Jetpack Compose?
Let’s break it down like friends talking over chai – with real-world examples, code patterns, and a sprinkle of developer wisdom. ๐ง
Learn how to convert a Website into Android App Using Jetpack Compose
๐ฆ What Is MVVM?
MVVM stands for Model-View-ViewModel. It's been the go-to architecture for Android devs for years.
Model: The data layer (Room DB, API, etc.)
View: The UI (Jetpack Compose screen)
ViewModel: Handles UI logic, exposes data using StateFlow, LiveData, etc.
๐ง How It Works in Compose:
@Composable
fun TodoScreen(viewModel: TodoViewModel = hiltViewModel()) {
val todos by viewModel.todoList.collectAsState()
LazyColumn {
items(todos) { todo ->
Text(todo.title)
}
}
}
Here, your ViewModel pushes state and the Composable observes it. Simple. Clean. Easy to test.
JETPACK COMPOSE STORY READER APP An app that speak
๐ What Is MVI?
MVI stands for Model-View-Intent. It’s inspired by reactive and functional programming (hello Redux fans!).
Model: The full UI state
View: Emits Intents (user actions)
Intent: Triggers an update to the Model
State: A single source of truth, often represented by a data class
๐ง How It Works in Compose:
data class TodoState(
val todos: List = emptyList(),
val isLoading: Boolean = false,
val error: String? = null
)
sealed class TodoIntent {
object LoadTodos : TodoIntent()
data class AddTodo(val title: String) : TodoIntent()
}
Composable observes TodoState, sends TodoIntent, and the ViewModel processes it via a reducer-style method.
๐งช Key Differences
Feature | MVVM | MVI |
---|---|---|
State Handling | Multiple sources (LiveData, StateFlow) | Single immutable state |
Unidirectional Flow | Optional | Strict |
Complex UI State | Can get messy | Centralized in one class |
Testing | ViewModel is easy to unit test | Slightly more boilerplate but predictable |
Learning Curve | Easy for beginners | Takes time to grasp reducer pattern |
Tooling Support | Works well with Jetpack ecosystem | No native tooling but clean logic |
๐ฅ Real-World Analogy
Imagine you’re running a tea shop.
MVVM:
- You (UI) talk to your staff (ViewModel).
- Your staff manages orders and tells the kitchen (Model).
- They give you updates when tea is ready.
- Simple, but if the tea machine (state) is acting weird, you might get confused where the delay happened.
MVI:
- You write everything in a logbook (Intent: MakeTea, ServeTea, etc.)
- The logbook decides how to react, step by step.
- The state of the tea shop is always in one place.
- More setup, but very predictable and traceable.
✅ When to Use What?
Use Case | Recommendation |
---|---|
๐ Small to Medium Apps | MVVM is perfect |
๐ Complex UI State (forms, tabs, dynamic UI) | MVI gives better control |
๐งช You want testability and time travel debugging | MVI wins here |
๐ง Team already knows MVVM | Stick with MVVM for faster delivery |
๐งฑ You love functional programming | Try MVI—you’ll enjoy it! |
⚔️ My Personal Verdict (as a dev)
If you’re building apps like Notes, Recipes, or E-commerce, MVVM is quick, intuitive, and works beautifully with Jetpack Compose.
But if you’re working on something like a real-time dashboard, game UI, or forms with lots of branching logic, MVI helps you avoid future headaches.
Use the right tool for the right job. Architecture isn’t religion—it’s a strategy. ๐งฉ
๐ฏ Final Thoughts
Jetpack Compose doesn’t force one pattern over another. That’s its beauty.
Whether you go MVVM or MVI, consistency and clean state management are what matter most.
How to use Firebase Google Sign in?
Let’s Build a Counter App using MVI
We understood what MVI (Model-View-Intent) is and how it brings structure and predictability to our Jetpack Compose apps.
Now, let’s build our first real app using MVI—a clean, reactive Counter App with increment, decrement, and reset features.
๐ฏ What We'll Build
A simple counter app with three buttons:
- ➕ Increase Count
- ➖ Decrease Count
- ๐ Reset to Zero
We'll use:
- Immutable State
- Sealed Intent classes
- A ViewModel to process intents and update state
- Jetpack Compose for the UI
๐ง MVI Components Recap
Before diving into code, here's how MVI works:
Component | Role |
---|---|
Intent | User actions (Button clicked, Text changed) |
State | Full representation of the UI |
ViewModel | Processes intents, updates state |
View (UI) | Reads state, sends intents |
1. ๐ฆ Create the UI State
data class CounterState(
val count: Int = 0
)
2. ๐ฆ Define Intents
sealed class CounterIntent {
data object Increment : CounterIntent()
data object Decrement : CounterIntent()
data object Reset : CounterIntent()
}
3. ๐ง ViewModel: The Reducer and State Flow
package com.example.codingcompose
import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
class CounterViewModel : ViewModel() {
private val _state = MutableStateFlow(CounterState())
val state: StateFlow = _state.asStateFlow()
fun onIntent(intent: CounterIntent) {
when (intent) {
is CounterIntent.Increment -> {
_state.value = _state.value.copy(count = _state.value.count + 1)
}
is CounterIntent.Decrement -> {
_state.value = _state.value.copy(count = _state.value.count - 1)
}
is CounterIntent.Reset -> {
_state.value = _state.value.copy(count = 0)
}
}
}
}
4. ๐จ UI in Jetpack Compose
package com.example.codingcompose
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
@Composable
fun CounterScreen(viewModel: CounterViewModel = viewModel()) {
val state by viewModel.state.collectAsState()
Column(
modifier = Modifier
.fillMaxSize()
.padding(32.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("Count: ${state.count}", fontSize = 32.sp,
fontWeight = FontWeight.Bold)
Spacer(modifier = Modifier.height(24.dp))
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
Button(onClick = { viewModel.onIntent(CounterIntent.Decrement) }) {
Text("➖")
}
Button(onClick = { viewModel.onIntent(CounterIntent.Reset) }) {
Text("๐")
}
Button(onClick = { viewModel.onIntent(CounterIntent.Increment) }) {
Text("➕")
}
}
}
}
๐ Result
A smooth, reactive counter app that follows unidirectional flow with centralized state—this is the power of MVI.
Jetpack Compose Counter App using MVVM
Let’s rebuild the same Counter App using MVVM (Model-View-ViewModel), so you and your readers can compare both patterns clearly.
๐ฆ MVVM Architecture
Layer | Responsibility |
---|---|
Model | The data (we won’t need a backend for this simple app) |
ViewModel | Holds business logic and exposes UI state |
View (UI) | Displays UI and observes state from ViewModel |
✅ MVVM Counter App in Jetpack Compose
1. ๐ง ViewModel with MutableInState
package com.example.counterapp
import androidx.compose.runtime.IntState
import androidx.compose.runtime.mutableIntStateOf
import androidx.lifecycle.ViewModel
class CounterMvvmViewModel : ViewModel() {
private val _count = mutableIntStateOf(0)
val count: IntState = _count
fun increment() {
_count.intValue++
}
fun decrement() {
_count.intValue--
}
fun reset() {
_count.intValue = 0
}
}
๐ Simpler and more direct than MVI—each function mutates the state.
2. ๐จ Composable UI
package com.example.counterapp
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.material3.ExtendedFloatingActionButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
@Composable
fun CounterScreen(viewModel: CounterMvvmViewModel = viewModel()) {
val count = viewModel.count.intValue
Column(
modifier = Modifier
.systemBarsPadding()
.fillMaxSize()
.padding(32.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("Count: $count", fontSize = 32.sp)
Spacer(modifier = Modifier.height(24.dp))
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
ExtendedFloatingActionButton(onClick = { viewModel.decrement() }) {
Text("➖")
}
ExtendedFloatingActionButton(onClick = { viewModel.reset() }) {
Text("๐ Reset")
}
ExtendedFloatingActionButton(onClick = { viewModel.increment() }) {
Text("➕")
}
}
}
}
OUTPUT:
๐ฏ My Final Thoughts
- Use MVVM when building quick, readable, smaller UIs.
- Use MVI when scaling to complex state, side-effects, or reactive patterns.