MVI Architecture in Android — Complete Guide
1. What is MVI Architecture?
MVI stands for Model–View–Intent. It is a reactive architecture pattern that enforces unidirectional data flow and is ideal for modern Android apps using Jetpack Compose or reactive frameworks like Flow or RxJava.
2. Key Components of MVI
- Model: Represents the state of the UI. Usually immutable and updated only via events.
- View: Observes the Model (UI state) and renders it. All user interactions are sent as Intents.
- Intent: Represents user actions or events (e.g., button clicks). Intents are processed to generate a new Model state.
3. MVI Data Flow
- User interacts with the View (Intent).
- The Intent is processed by a Reducer / ViewModel.
- The Model state is updated based on the Intent.
- The View observes the new Model state and updates the UI.
- The cycle repeats for the next Intent.
4. Advantages of MVI
- Unidirectional flow eliminates UI inconsistencies.
- Single source of truth — the Model represents the entire UI state.
- Reactive — works perfectly with Jetpack Compose, StateFlow, or LiveData.
- Easier testing — Intents and Model states can be tested independently of the View.
5. Example: Login Screen Using MVI
📁 Folder Structure
├── model/
│ └── LoginState.kt
├── intent/
│ └── LoginIntent.kt
├── viewmodel/
│ └── LoginViewModel.kt
├── view/
│ └── LoginActivity.kt
└── activity_login.xml
🧩 Model: LoginState.kt
package com.example.mvi.model
data class LoginState(
val username: String = "",
val password: String = "",
val isLoading: Boolean = false,
val errorMessage: String? = null,
val success: Boolean = false
)
🎯 Intent: LoginIntent.kt
package com.example.mvi.intent
sealed class LoginIntent {
data class Submit(val username: String, val password: String) : LoginIntent()
}
🧠 ViewModel: LoginViewModel.kt
package com.example.mvi.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.mvi.model.LoginState
import com.example.mvi.intent.LoginIntent
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
class LoginViewModel : ViewModel() {
private val _uiState = MutableStateFlow(LoginState())
val uiState: StateFlow get() = _uiState
fun processIntent(intent: LoginIntent) {
when (intent) {
is LoginIntent.Submit -> login(intent.username, intent.password)
}
}
private fun login(username: String, password: String) {
viewModelScope.launch {
_uiState.value = _uiState.value.copy(isLoading = true, errorMessage = null)
// Simulate login
if (username.isNotEmpty() && password.length >= 6) {
_uiState.value = LoginState(username, password, isLoading = false, success = true)
} else {
_uiState.value = LoginState(username, password, isLoading = false, errorMessage = "Invalid credentials")
}
}
}
}
🎬 View: LoginActivity.kt (Jetpack Compose Example)
package com.example.mvi.view
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.material.*
import androidx.compose.runtime.*
import com.example.mvi.viewmodel.LoginViewModel
import com.example.mvi.intent.LoginIntent
class LoginActivity : ComponentActivity() {
private val viewModel: LoginViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val uiState by viewModel.uiState.collectAsState()
LoginScreen(uiState) { username, password ->
viewModel.processIntent(LoginIntent.Submit(username, password))
}
}
}
}
@Composable
fun LoginScreen(state: com.example.mvi.model.LoginState, onSubmit: (String, String) -> Unit) {
var username by remember { mutableStateOf("") }
var password by remember { mutableStateOf("") }
Column {
TextField(value = username, onValueChange = { username = it }, label = { Text("Username") })
TextField(value = password, onValueChange = { password = it }, label = { Text("Password") })
Button(onClick = { onSubmit(username, password) }) { Text("Login") }
if (state.isLoading) { CircularProgressIndicator() }
state.errorMessage?.let { Text(it, color = MaterialTheme.colors.error) }
if (state.success) { Text("Login Successful!") }
}
}
6. Advantages of MVI
- Unidirectional data flow prevents UI inconsistencies.
- Single source of truth — the UI state is centralized in the Model.
- Reactive and perfect for Compose or Flow.
- Easier testing of Intents and Model states independently of View.
Conclusion
MVI is a modern reactive architecture that fits perfectly with Jetpack Compose. By enforcing unidirectional data flow and a single source of truth, it simplifies state management and testing, making it ideal for large and complex applications.