MVVM vs MVI in Jetpack Compose – Which One Should You Use?
Learn how to convert a Website into Android App Using Jetpack Compose
π¦ What Is MVVM?
π§ How It Works in Compose:
@Composable
fun TodoScreen(viewModel: TodoViewModel = hiltViewModel()) {
val todos by viewModel.todoList.collectAsState()
LazyColumn {
items(todos) { todo ->
Text(todo.title)
}
}
}
JETPACK COMPOSE STORY READER APP An app that speak
π What Is MVI?
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()
}
π§ͺ 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
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)
π― Final Thoughts
How to use Firebase Google Sign in?
Let’s Build a Counter App using MVI
π― What We'll Build
- ➕ 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
| 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
Jetpack Compose Counter App using MVVM
π¦ 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
}
}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.


