MVI Architecture for Jetpack Compose Apps
Building apps with Jetpack Compose? Then you’ve got to check out the Model-View-Intent (MVI) architecture! It’s a powerful way to handle UI state and user interactions, keeping everything clean and simple.
Lets first understand how a Jetpack Compose UI works:
- In Jetpack Compose, the UI is immutable — once drawn, it cannot be updated directly.
- What you can control is the state of your UI.
- Every time the state changes, Compose automatically recreates the parts of the UI tree that have changed.
- Composables accept state and expose events.
- For example, a
TextField
accepts a value and provides a callback,onValueChange
, that requests the callback handler to update the value.
🧩 What is MVI Architecture?
MVI stands for Model-View-Intent, and it consists of three core components:
- Model: Represents the state of the UI. It’s immutable, and whenever the state changes, a new model is created.
- View: The UI, which observes the model and renders itself based on the model’s state.
- Intent: Actions triggered by the user that initiate the flow of data, which eventually updates the model.
🔄 MVI Workflow
The MVI architecture revolves around unidirectional data flow. Here’s how it works:
- User Intent: A user action (e.g., button click) triggers an intent.
- Processing Intent: The intent is passed to a ViewModel (or a similar state handler), where business logic is executed, such as making network requests.
- State Update: The result of the processing updates the model (state).
- View Rendering: The view (Jetpack Compose UI) observes changes to the model and re-renders itself accordingly.
🛠 Code Example
Let’s build a simple example where we fetch data from a repository and display it in a list, following the MVI architecture.
Step 1. Defining the Model (UI State)
We start by defining the UiState, which represents the different states our UI can be in.
sealed class UiState {
object Loading : UiState()
data class Success(val items: List<String>) : UiState()
data class Error(val message: String) : UiState()
}
Step 2. Defining the Intent
The intent represents the user actions. For this example, we define an intent for fetching items.
sealed class UserIntent {
object FetchItems : UserIntent()
}
Step 3. ViewModel to Handle the State and Intent
The ViewModel will manage the state and handle the user intents by interacting with a repository.
class MainViewModel : ViewModel() {
private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
val uiState: StateFlow<UiState> = _uiState
fun handleIntent(intent: UserIntent) {
when (intent) {
is UserIntent.FetchItems -> fetchItems()
}
}
private fun fetchItems() {
viewModelScope.launch {
try {
_uiState.value = UiState.Loading
val items = repository.getItems() // Simulate repository call
_uiState.value = UiState.Success(items)
} catch (e: Exception) {
_uiState.value = UiState.Error("Failed to fetch items")
}
}
}
}
Step 4. The View (Jetpack Compose UI)
The UI observes the uiState from the ViewModel and reacts to state changes.
@Composable
fun ItemListScreen(viewModel: MainViewModel) {
val uiState by viewModel.uiState.collectAsState()
when (uiState) {
is UiState.Loading -> {
CircularProgressIndicator()
}
is UiState.Success -> {
LazyColumn {
items((uiState as UiState.Success).items) { item ->
Text(text = item)
}
}
}
is UiState.Error -> {
Text(text = (uiState as UiState.Error).message)
}
}
}
Step 5. Triggering the Intent
To trigger the intent, we can set up an action like a button click that tells the ViewModel to fetch the items.
@Composable
fun FetchButton(viewModel: MainViewModel) {
Button(onClick = { viewModel.handleIntent(UserIntent.FetchItems) }) {
Text("Fetch Items")
}
}
Step 6. Bringing It All Together
Now, we combine everything into a single composable function that includes the button and the item list.
@Composable
fun MainScreen(viewModel: MainViewModel = viewModel()) {
Column {
FetchButton(viewModel)
ItemListScreen(viewModel)
}
}
🔚 Conclusion
- MVI makes managing UI state and actions easier by using a one-way data flow.
- This fits well with Jetpack Compose because it automatically updates the UI when the state changes.
- Using MVI helps make your apps more predictable, easier to test, and more scalable, especially for complex projects.
- With this example, you now understand how to use MVI in Jetpack Compose.
If you want your Compose apps to be more organized, testable, and easy to maintain, MVI is definitely worth exploring! 💡
Thank you for taking the time to read this article. If you found the information valuable, please consider giving it a clap or sharing it with others who might benefit from it.
Any Suggestions are welcome. If you need any help or have questions for Code Contact US. You can follow us on LinkedIn for more updates 🔔