Android Core

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

  1. User interacts with the View (Intent).
  2. The Intent is processed by a Reducer / ViewModel.
  3. The Model state is updated based on the Intent.
  4. The View observes the new Model state and updates the UI.
  5. 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.