Is MVVM or MVI the pattern Android recommends?
MVVM: Several Streams
catch. It names IOException, not Exception. A broad catch in a coroutine also swallows cancellation, which is covered in the coroutines deep dive.class ProfileViewModel(
private val userRepo: UserRepository
) : ViewModel() {
private val _user = MutableStateFlow<User?>(null)
val user: StateFlow<User?> = _user.asStateFlow()
private val _isLoading = MutableStateFlow(false)
val isLoading: StateFlow<Boolean> = _isLoading.asStateFlow()
private val _error = MutableStateFlow<String?>(null)
val error: StateFlow<String?> = _error.asStateFlow()
fun loadProfile(userId: String) {
viewModelScope.launch {
_isLoading.value = true
_error.value = null
try {
_user.value = userRepo.getUser(userId)
} catch (e: IOException) {
_error.value = e.message
} finally {
_isLoading.value = false
}
}
}
}MVI: One Stream
RetryLoad intent that did nothing; an intent with no handler is the MVI version of a dead button, so the sample now remembers the last request.// State: one immutable object for the whole screen
data class ProfileState(
val user: User? = null,
val isLoading: Boolean = false,
val error: String? = null
)
// Intent: every action the user can take
sealed interface ProfileIntent {
data class LoadProfile(val userId: String) : ProfileIntent
data object RetryLoad : ProfileIntent
data object DismissError : ProfileIntent
}
class ProfileViewModel(
private val userRepo: UserRepository
) : ViewModel() {
private val _state = MutableStateFlow(ProfileState())
val state: StateFlow<ProfileState> = _state.asStateFlow()
private var lastUserId: String? = null
fun onIntent(intent: ProfileIntent) {
when (intent) {
is ProfileIntent.LoadProfile -> loadProfile(intent.userId)
ProfileIntent.RetryLoad -> lastUserId?.let(::loadProfile)
ProfileIntent.DismissError ->
_state.update { it.copy(error = null) }
}
}
private fun loadProfile(userId: String) {
lastUserId = userId
viewModelScope.launch {
_state.update { it.copy(isLoading = true, error = null) }
try {
val user = userRepo.getUser(userId)
_state.update { it.copy(user = user, isLoading = false) }
} catch (e: IOException) {
_state.update {
it.copy(isLoading = false, error = e.message)
}
}
}
}
}What Google Actually Says About One Stream or Several
| Question | Points to one stream (MVI style) | Points to several (MVVM style) |
|---|---|---|
| Do the values describe one situation? | Yes: loading, data and error for the same request | No: a profile and an unrelated notification badge |
| Does one value change far more often? | No | Yes: a timer or scroll position beside slow data |
| Does a change in one require a change in another? | Yes | No |
Using Both Together
collectAsStateWithLifecycle(), the lifecycle-aware collector.// The composable is the same shape for either pattern
@Composable
fun ProfileScreen(viewModel: ProfileViewModel) {
// MVI style: one state object
val state by viewModel.state.collectAsStateWithLifecycle()
ProfileContent(
user = state.user,
isLoading = state.isLoading,
error = state.error,
onRetry = { viewModel.onIntent(ProfileIntent.RetryLoad) },
onDismissError = {
viewModel.onIntent(ProfileIntent.DismissError)
}
)
}Key Takeaways
- 1Google's architecture guide names neither MVVM nor MVI. It recommends a single source of truth and unidirectional data flow.
- 2The practical difference is whether the ViewModel exposes screen state as several streams or as one immutable object.
- 3One stream gives consistency: the UI can never observe half an update.
- 4Several streams suit unrelated data, or data where one value updates far more often than the rest.
- 5Decide per screen. The ViewModel is the boundary, so the two styles coexist in one app.
Frequently Asked
Should I migrate from MVVM to MVI?
Only for screens where separate streams have produced inconsistent states, such as a spinner and an error showing together. Convert those screens to a single state object and leave the rest. The ViewModel is the boundary, so one screen can change style without any other screen noticing or needing a rewrite.
Can I use both MVVM and MVI in the same app?
Yes. Google's guidance is that the choice between one state stream and several depends on how the emitted items relate to each other, which is a per-screen question. Use one state object where values depend on each other, and separate streams where they are unrelated or update at very different rates.
Does MVI work with Jetpack Compose?
Yes. A composable collects one StateFlow with collectAsStateWithLifecycle() and sends events back as intents. One caution from the UI layer guide applies: every emission of the state object updates the UI that reads it, so a very large state object that changes often can cause more work than separate streams would.
Ready to architect your next Android app?
ANDROID-ARCHITECT generates production-ready Kotlin code, architecture blueprints, and CI/CD configurations from plain-language descriptions. Start building for free.