How Can MVI Architecture Revolutionize Your Compose Multiplatform Development?

Akbar Dzulfikar
4 min read5 days ago

--

As cross-platform development grows in popularity, building a robust and scalable architecture becomes crucial. Enter MVI (Model-View-Intent) — a powerful architecture pattern that promotes predictable state management. In this guide, we’ll explore how to implement MVI in a Compose Multiplatform project using JetBrains’ experimental ViewModel support. This setup allows developers to share business logic across Android and iOS seamlessly.

Why MVI for Compose Multiplatform?

MVI stands out in Compose for several reasons:

  1. Predictable State Management: MVI ensures a unidirectional data flow, making state transitions predictable and easier to debug.
  2. Simplified UI Logic: By decoupling the state from the UI, MVI makes complex UI interactions simpler to implement and maintain.
  3. Scalable and Testable: The separation of concerns in MVI makes it easy to test each component in isolation. This modularity is crucial for large applications.
  4. Shared Business Logic: In Compose Multiplatform, sharing ViewModel logic between Android and iOS reduces duplication and development time, leading to a more maintainable codebase.

These advantages make MVI a compelling choice for building scalable and maintainable Compose Multiplatform applications.

Setting Up Your Compose Multiplatform Project

Before diving into the implementation, ensure you have a Compose Multiplatform project set up. Add the following dependency to your commonMain source set:

kotlin {
sourceSets {
commonMain {
dependencies {
implementation("org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose:2.8.0")
}
}
}
}

Note: The lifecycle-viewmodel-compose dependency is currently experimental, and its API may change in future updates. Make sure to stay updated with the latest releases.

Implementing the MVI Pattern

Let’s break down the implementation of MVI in a Compose Multiplatform project.

1. Define the Model

The Model represents the state of the UI. We’ll use a sealed class to define different states:

sealed class State<out T> {
object Idle : State<Nothing>()
object Loading : State<Nothing>()
data class Success<out T>(val data: T) : State<T>()
data class Failure(val throwable: Throwable) : State<Nothing>()
}

This structure allows us to represent various UI states like Idle, Loading, Success, and Failure.

2. Create User Intents

Intents represent user actions or events. For example, a button click to fetch data from an API.

sealed class AppIntent {
object GetApi : AppIntent()
}

3. Build the ViewModel

The ViewModel handles the business logic and updates the state based on user intents. It interacts with a repository to fetch data and manages state transitions.

class AppViewModel : ViewModel() {
private val networkRepository = NetworkRepository()
private val mutableStateData: MutableStateFlow<State<ReqresResponse>> = MutableStateFlow(State.Idle)
val stateData: StateFlow<State<ReqresResponse>> = mutableStateData
fun handleIntent(appIntent: AppIntent) {
when (appIntent) {
is AppIntent.GetApi -> getApi()
}
}
private fun getApi() = viewModelScope.launch {
mutableStateData.value = State.Loading
try {
val response = networkRepository.getUser()
mutableStateData.value = State.Success(response)
} catch (e: Exception) {
mutableStateData.value = State.Failure(e)
}
}
}

In this example:

  • mutableStateData is used to hold the current state.
  • The handleIntent function processes incoming intents and triggers the appropriate actions.
  • getApi fetches data and updates the state based on the result.

4. Creating the UI with Composables

The UI reacts to the state changes in the ViewModel. Let’s create a simple UI that displays different content based on the current state.

@Composable
@Preview
fun App(
viewModel: AppViewModel = viewModel { AppViewModel() }
) {
MaterialTheme {
val reqresResponseState by viewModel.stateData.collectAsState()
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Button(
onClick = {
viewModel.handleIntent(AppIntent.GetApi)
}
) {
Text(text = "Fetch API Data")
}
Spacer(modifier = Modifier.height(12.dp))
when (reqresResponseState) {
is State.Idle -> {
Text(text = "Click the button to fetch data.")
}
is State.Loading -> {
CircularProgressIndicator()
}
is State.Success -> {
val response = (reqresResponseState as State.Success).data
Text(text = "Data: ${response.data}")
}
is State.Failure -> {
val error = (reqresResponseState as State.Failure).throwable
Text(text = "Error: ${error.localizedMessage}")
}
}
}
}
}

Here’s what’s happening:

  • We use collectAsState() to observe changes in the ViewModel’s state.
  • The UI updates based on the current state, such as showing a progress indicator during loading or displaying the fetched data.

Handling Side Effects

In MVI, side effects like API calls should be managed outside the UI. This separation ensures the UI remains simple and declarative. In our example, the getApi function handles this by using coroutines to fetch data and update the state.

Testing the MVI Pattern

Testing is crucial for validating your architecture. Here are some strategies:

  1. Unit Testing ViewModel: Test the ViewModel logic independently by verifying state transitions.
  2. UI Testing: Use Compose’s testing framework to simulate user interactions and validate UI behavior.

Challenges and Tips

  • State Explosion: Managing multiple states can be overwhelming. Group related states or use data classes to simplify.
  • Experimental Features: Be cautious of breaking changes in experimental libraries. Stay updated with the library’s development.

Conclusion

MVI in Compose Multiplatform offers a robust, scalable solution for building complex UIs with shared business logic. By embracing unidirectional data flow and clear state management, MVI makes your app predictable, maintainable, and testable. Start integrating MVI in your Compose Multiplatform projects today and experience the benefits of this powerful architecture.

Bio : Laksamana Akbar Dzulfikar,

Senior Android Engineer

and I’m open to work.

--

--