Using Jetpack Compose With MVI Architecture
MVI offers a structured, one-way data flow, aligning with Jetpack Compose's reactive design and ensuring clear state handling, easier debugging, and better scalability.
Join the DZone community and get the full member experience.
Join For FreeUnderstanding MVVM and MVI
MVVM (Model-View-ViewModel)
MVVM is one of the most popular architecture patterns in Android development. It helps keep UI logic separate from business logic by using a ViewModel, which acts as a bridge between the View (UI) and the Model (data and logic). The View listens for updates from the ViewModel and updates the UI when needed.
- Model: Handles data and business logic.
- View: Displays the UI and passes user actions to the ViewModel.
- ViewModel: Manages data for the View and responds to user interactions.
MVI (Model-View-Intent)
MVI follows a reactive, unidirectional data flow, meaning UI updates happen in a predictable way from a single source of truth. This makes state management clearer and more structured.
- Model: Represents the UI's state at any given time.
- View: Displays the UI and sends user actions (intents) to the system.
- Intent: Captures user actions or events that lead to state changes.
The key difference is that MVI treats the UI as a single, reactive state, while MVVM allows multiple pieces of data to be observed separately.
Why MVI Works Better Than MVVM in Jetpack Compose
1. Smooth Unidirectional Data Flow
MVI enforces a clear, one-way data flow that aligns perfectly with Jetpack Compose’s reactive nature. Here’s how it works:
- The View sends user actions (Intents) to the ViewModel.
- The ViewModel processes these actions and updates the State.
- The View observes the updated state and re-renders accordingly.
This structured approach keeps the UI in sync with the state, making the app more predictable and easier to debug.
2. A Single Source of Truth
MVI represents the entire UI state in one immutable data structure, preventing inconsistent or fragmented states. In MVVM, multiple LiveData or StateFlow objects often track different UI elements, which can lead to inconsistencies.
By keeping everything in one state object, MVI makes state management clearer and more maintainable.
3. Better State Handling
Jetpack Compose is built around immutable state, and MVI naturally supports this with its reducer-based approach:
- State updates happen through pure functions that take the current state and an intent, then return a new state.
- This ensures state changes are predictable and easy to test.
On the other hand, MVVM often relies on mutable state (LiveData, StateFlow), which can introduce side effects and make the code harder to maintain.
Implementing MVI With Jetpack Compose
Let's build a basic messaging app together using Jetpack Compose with MVI architecture.
1. Define the Model
The model represents the state of the UI. In this case, it will hold the list of messages:
data class Message(val id: Int, val text: String, val isSentByMe: Boolean)
sealed class ChatState {
object Loading : ChatState()
data class Success(val data: List<Message>) : ChatState()
data class Error(val message: String) : ChatState()
}
2. Define Intent
User actions are represented as intents:
sealed class ChatIntent {
object LoadData : ChatIntent()
data class SendMessage(val text: String) : ChatIntent()
}
3. Create ViewModel With StateFlow
We use StateFlow
to manage state and MutableSharedFlow
for user intents:
class ChatViewModel : ViewModel() {
private val _state = MutableStateFlow<ChatState>(ChatState.Loading)
val state: StateFlow<ChatState> = _state.asStateFlow()
private val _intentFlow = MutableSharedFlow<ChatIntent>()
init {
processIntents()
}
fun sendIntent(intent: ChatIntent) {
viewModelScope.launch { _intentFlow.emit(intent) }
}
private fun processIntents() {
viewModelScope.launch {
_intentFlow.collect { intent ->
when (intent) {
is ChatIntent.LoadData -> loadData()
is ChatIntent.SendMessage -> sendMessage(intent.text)
}
}
}
}
fun processIntent(intent: ChatIntent) {
when (intent) {
is ChatIntent.SendMessage -> sendMessage(intent.text)
}
}
private fun loadData() {
viewModelScope.launch {
_state.value = try {
val data = listOf(
Message(id = 1, text = "hello", isSentByMe = true)
)
UiState.Success(data)
} catch (e: Exception) {
UiState.Error("Failed to load data")
}
}
}
private fun sendMessage(text: String) {
val newMessage = Message(
id = _state.value.messages.size + 1,
text = text,
isSentByMe = true
)
_state.update { it.copy(messages = it.messages + newMessage) }
}
}
4. Compose UI With State Handling
Now, we build the UI in Jetpack Compose:
@Composable
fun ChatScreen(viewModel: ChatViewModel) {
val state by viewModel.state.collectAsState()
Column(modifier = Modifier.fillMaxSize()) {
MessageList(messages = state.messages)
MessageInput { text ->
viewModel.processIntent(ChatIntent.SendMessage(text))
}
}
}
@Composable
fun MessageList(messages: List<Message>) {
LazyColumn(modifier = Modifier.weight(1f)) {
items(messages) { message ->
MessageItem(message = message)
}
}
}
@Composable
fun MessageItem(message: Message) {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
contentAlignment = if (message.isSentByMe) Alignment.CenterEnd else Alignment.CenterStart
) {
Text(
text = message.text,
modifier = Modifier
.background(if (message.isSentByMe) Color.Blue else Color.Gray, shape = RoundedCornerShape(8.dp))
.padding(8.dp),
color = Color.White
)
}
}
@Composable
fun MessageInput(onSend: (String) -> Unit) {
var text by remember { mutableStateOf("") }
Row(modifier = Modifier.fillMaxWidth()) {
TextField(
value = text,
onValueChange = { text = it },
modifier = Modifier.weight(1f),
placeholder = { Text("Type a message") }
)
Button(onClick = {
onSend(text)
text = ""
}) {
Text("Send")
}
}
}
5. Trigger Intent From UI
To trigger an intent, update the UI to send an event to the ViewModel
:
@Composable
fun ChatScreen(viewModel: ChatViewModel) {
val state by viewModel.state.collectAsState()
LaunchedEffect(Unit) {
viewModel.sendIntent(UiIntent.LoadData)
}
Column(modifier = Modifier.fillMaxSize()) {
MessageList(messages = state.messages)
MessageInput { text ->
viewModel.processIntent(ChatIntent.SendMessage(text))
}
}
}
Summary
MVVM has been a solid choice for Android development, but MVI provides a more modern and scalable approach, especially when used with Jetpack Compose. Its structured one-way data flow, single source of truth, and focus on immutable state make it a perfect match for Compose’s declarative and reactive design.
By switching to MVI, developers can create apps that are more predictable, easier to maintain, and simpler to test. As Jetpack Compose becomes the standard for Android UI development, MVI is likely to become the preferred architecture for building modern, scalable apps.
Opinions expressed by DZone contributors are their own.
Comments