MVI Made Easy: A Beginner’s Guide to MVI with Example and comparison with MVVM
Agenda:
- What is MVI Architecture?
- Components of MVI Architecture (Detailed explanation using Basic Counter App)
- MVI Core Principles
- Benefits of MVI Architecture
- Disadvantages of MVI Architecture
- MVVM vs MVI
1. What is MVI ?
- MVI stands for Model View Intent.
- MVI (also MVVM) is used to separate the presentation layer of app and NOT the whole app.
- MVI is a unidirectional data flow architecture that promotes clear separation of concerns, testability, and maintainability.
- The goal of MVI is that each layer should not take on a task other than sending/receiving data, they should be independent of each other.
2. Components of MVI Architecture (overview)
It has 3 major components :
i) Model: Represents the state of the UI. This is typically an immutable data class that contains all the data needed to render the UI.
ii) View: A function that renders the UI based on the Model’s state. In Jetpack Compose, this is achieved using composable functions.
iii) Intent: Represents user actions or events that occur in the UI. Intents are typically sealed classes that encapsulate different types of events.
Now Let’s Discuss MVI in detail
Let’s Understand it through an example: Basic Counter App
This app will have 5 components just like any other MVI Project:
- Model
- Intent
- Reducer
- ViewModel
- View
1. Model:
- Encapsulates the business logic and data of your application. In Compose, this could involve data classes, repositories and any necessary business operations.
- MVI aims towards models that not only hold data but also state in the models.
- As it holds both the data and represent the app state, they should be immutable to ensure data flow between them and between other layers.
- In short, the Model (State) represents the data structure.
In this case, it’s the counter value.
//CounterState.kt
data class CounterState(val count: Int = 0)
2. Intent:
- First of all, Intents are NOT Android’s android.content.Intent class.
- Intents in MVI represent a future action that changes the state of the application.
- It takes the user interactions from the view, converts them into a format that the model can understand, and transmits them to the model layer.
- In short, it is a structure that encapsulates the user’s interaction.
- It represents user interactions or events that trigger state updates. In Compose, this could be button clicks, text input changes, or navigation events.
- In short, Intents represent user actions or events.
For this example, we’ll have intents for incrementing and decrementing the counter.
//CounterIntent.kt
sealed class CounterIntent {
object Increment : CounterIntent()
object Decrement : CounterIntent()
}
3. Reducer:
- The Reducer is a pure function that takes the current state and an intent, then returns a new state.
//CounterReducer.kt
fun counterReducer(state: CounterState, intent: CounterIntent): CounterState {
return when (intent) {
is CounterIntent.Increment -> state.copy(count = state.count + 1)
is CounterIntent.Decrement -> state.copy(count = state.count - 1)
}
}
4. ViewModel:
- The ViewModel manages the state and handles intents. It holds the state and uses the reducer to update it based on the received intents.
//CounterViewModel.kt
class CounterViewModel : ViewModel() {
private val _uiState = MutableStateFlow(CounterState())
val uiState: StateFlow<CounterState> = _uiState
fun handleIntent(intent: CounterIntent) {
_uiState.value = counterReducer(_uiState.value, intent)
}
}
5. View:
- Responsible for displaying the UI based on the current state.
- Jetpack Compose’s declarative nature aligns well with MVI’s state-driven approach.
- You create composable functions that consume the state and render the UI accordingly.
- It brings the information it receives from the model together with the user in the user interface.
- It captures the user’s interactions and transmits them to the intent layer.
- In short, the View in Jetpack Compose observes the state from the ViewModel and sends user actions as intents.
//MainActivity.kt
class MainActivity : ComponentActivity() {
private val viewModel: CounterViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val uiState by viewModel.uiState.collectAsState()
CounterScreen(uiState = uiState, onIntent = viewModel::handleIntent)
}
}
}
//CounterScreen.kt
@Composable
fun CounterScreen(uiState: CounterState, onIntent: (CounterIntent) -> Unit) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(text = "Count: ${uiState.count}", style = MaterialTheme.typography.h4)
Row {
Button(onClick = { onIntent(CounterIntent.Decrement) }) {
Text("Decrement")
}
Spacer(modifier = Modifier.width(16.dp))
Button(onClick = { onIntent(CounterIntent.Increment) }) {
Text("Increment")
}
}
}
}
Summary of above Counter App
- Model (State): Represents the data (
CounterState
). - Intent: Represents user actions/events (
CounterIntent
). - Reducer: Processes intents to produce new states (
counterReducer
function). - ViewModel: Manages state and handles intents (
CounterViewModel
). - View: Observes state and sends user actions as intents (
CounterScreen
).
3. MVI Core Principles
i) Functionality: We need functionality to provide a direct output based on user interaction. Functional programming returns a direct value and this helps us for a fast process.
ii) Immutable: Immutable objects make thread management easier.
iii) Reactive: Reactive programming also safely parallelizes work (using Kotlin flows).
4. Benefits of MVI Architecture
- Uni-direction data flow.
- Separation of Concerns: Clears division between UI, business logic, and data handling.
- Testability: Easier to write unit tests for ViewModels and Models since they don’t rely on external UI elements.
- Composability Alignment: MVI’s state-centric approach aligns well with Compose’s declarative UI paradigm.
5. Disadvantages of MVI Architecture
- Increases Boilerplate code : Separately created intents and states can increase boilerplate code at a certain point.
- Complexity: Complexity may increase due to boilerplate codes, especially as the application grows.
6. MVVM vs MVI
Hurray, That`s it. If you have reached here, it means you have learned MVI architecture (at least you can start building basic projects).
I hope you have enjoyed reading this as much as I`ve enjoyed writing it. If you believe this tutorial will help someone, do not hesitate to share! you can Smash the clap up to 50 times, and let other people know about MVI Architecture.