MVVM vs MVI: Qual Arquitetura Escolher em 2026?
Comparação aprofundada de MVVM e MVI no Android: vantagens, desvantagens, casos de uso e guia prático. Atualizado com os explicit backing fields do Kotlin 2.4.

Escolher a arquitetura correta é uma decisão crucial que impacta a manutenibilidade, a testabilidade e a escalabilidade de uma aplicação Android. Em 2026, dois padrões dominam o ecossistema: MVVM, o padrão da indústria, e MVI, a abordagem reativa que se tornou o ajuste natural para o Jetpack Compose.
Uma má escolha de arquitetura é cara: dívida técnica, bugs difíceis de reproduzir e refatorações dolorosas. Compreender os pontos fortes e fracos de cada abordagem evita muitas dores de cabeça a longo prazo.
Entendendo o MVVM: O Padrão Estabelecido
MVVM (Model-View-ViewModel) é a arquitetura recomendada pelo Google desde a introdução do Jetpack. Ela separa as responsabilidades em três camadas distintas, tornando o código mais organizado e testável.
Princípios Fundamentais do MVVM
O padrão MVVM baseia-se em uma separação clara: o Model gerencia dados e lógica de negócio, a View exibe a interface, e o ViewModel faz a ponte entre os dois expondo estados observáveis.
Este primeiro exemplo mostra a estrutura básica com um ViewModel que expõe estado observável e métodos para interações do usuário. A sintaxe de explicit backing fields do Kotlin 2.4 elimina o boilerplate tradicional do padrão _uiState / uiState.
// MVVM ViewModel with Kotlin 2.4 explicit backing fields
// The backing field syntax eliminates the _state / state dance
class UserProfileViewModel(
private val userRepository: UserRepository,
private val analyticsTracker: AnalyticsTracker
) : ViewModel() {
// Kotlin 2.4: explicit backing field - no more _uiState / uiState pair
val uiState: StateFlow<UserProfileState>
field = MutableStateFlow(UserProfileState())
// Separate loading state - MVVM allows multiple flows
val isLoading: StateFlow<Boolean>
field = MutableStateFlow(false)
// One-shot error messages via SharedFlow
private val _errorMessage = MutableSharedFlow<String>()
val errorMessage: SharedFlow<String> = _errorMessage.asSharedFlow()
// Initial profile loading
fun loadProfile(userId: String) {
viewModelScope.launch {
isLoading.value = true
try {
// Repository call to fetch data
val user = userRepository.getUser(userId)
// Update state with new data
uiState.update { currentState ->
currentState.copy(
user = user,
isEditing = false
)
}
// Analytics tracking
analyticsTracker.trackProfileViewed(userId)
} catch (e: Exception) {
// Emit one-shot error message
_errorMessage.emit("Unable to load profile")
} finally {
isLoading.value = false
}
}
}
// Enable edit mode
fun enableEditMode() {
uiState.update { it.copy(isEditing = true) }
}
// Save profile changes
fun saveProfile(name: String, bio: String) {
viewModelScope.launch {
isLoading.value = true
try {
val updatedUser = userRepository.updateUser(
uiState.value.user?.id ?: return@launch,
name = name,
bio = bio
)
uiState.update {
it.copy(user = updatedUser, isEditing = false)
}
} catch (e: Exception) {
_errorMessage.emit("Failed to save profile")
} finally {
isLoading.value = false
}
}
}
}
// Data class representing the screen state
data class UserProfileState(
val user: User? = null,
val isEditing: Boolean = false
)Este ViewModel ilustra a abordagem MVVM típica: múltiplos flows observáveis (estado principal, carregamento, erros) e métodos públicos para cada ação do usuário. A sintaxe de explicit backing fields do Kotlin 2.4 torna o código mais limpo ao declarar o tipo de propriedade como StateFlow enquanto o backing field é MutableStateFlow.
Vantagens do MVVM
O MVVM possui pontos fortes que explicam sua adoção massiva:
- Familiaridade: A maioria dos desenvolvedores Android conhece esse padrão
- Flexibilidade: A estrutura do estado é completamente livre
- Ecossistema: Integração perfeita com Jetpack (LiveData, StateFlow, Hilt)
- Simplicidade: Curva de aprendizado suave para iniciantes
O MVVM é especialmente adequado para equipes mistas com desenvolvedores de diferentes níveis. Sua simplicidade conceitual facilita a integração de novos membros.
Limitações do MVVM
Porém, o MVVM mostra suas limitações conforme a aplicação cresce. O principal problema é o gerenciamento de estado distribuído. Este exemplo ilustra o problema comum de fragmentação do estado:
// Example MVVM ViewModel with fragmented state
// This pattern becomes problematic as the screen grows in complexity
class CheckoutViewModel : ViewModel() {
// Problem: state scattered across multiple flows
val cart: StateFlow<List<CartItem>>
field = MutableStateFlow(emptyList())
val selectedAddress: StateFlow<Address?>
field = MutableStateFlow(null)
val selectedPayment: StateFlow<PaymentMethod?>
field = MutableStateFlow(null)
val promoCode: StateFlow<String?>
field = MutableStateFlow(null)
val isLoading: StateFlow<Boolean>
field = MutableStateFlow(false)
val error: StateFlow<String?>
field = MutableStateFlow(null)
// Each modification can create temporary inconsistent states
fun applyPromoCode(code: String) {
viewModelScope.launch {
isLoading.value = true
error.value = null
try {
val discount = promoRepository.validate(code)
promoCode.value = code
// Cart state also needs updating...
// but there's a delay between the two updates
recalculateCart()
} catch (e: Exception) {
error.value = e.message
promoCode.value = null
} finally {
isLoading.value = false
}
}
}
// Hard to guarantee consistency across all these states
private fun recalculateCart() {
// Complex logic depending on multiple states...
}
}Este exemplo mostra como o estado pode se fragmentar no MVVM, dificultando o rastreamento de transições e a reprodução de bugs.
Entendendo o MVI: A Abordagem Unidirecional
MVI (Model-View-Intent) adota uma filosofia diferente: fluxo de dados unidirecional e um único estado imutável. Essa abordagem, inspirada no Redux, elimina os problemas de estado inconsistente.
Princípios Fundamentais do MVI
No MVI, tudo segue um ciclo claro: o usuário emite um Intent (ação), o Reducer transforma o estado atual em um novo estado, e a View exibe esse único estado. É previsível, testável e depurável.
Esta implementação da mesma tela de perfil demonstra como o estado é centralizado e as ações são tipadas explicitamente:
// MVI ViewModel for the same user profile screen
// Note the structure: Intent -> Reducer -> Single State
class UserProfileMviViewModel(
private val userRepository: UserRepository,
private val analyticsTracker: AnalyticsTracker
) : ViewModel() {
// Single, immutable state - the absolute source of truth
val state: StateFlow<UserProfileState>
field = MutableStateFlow(UserProfileState())
// Channel for side effects (navigation, snackbar)
private val _sideEffect = Channel<UserProfileSideEffect>()
val sideEffect: Flow<UserProfileSideEffect> = _sideEffect.receiveAsFlow()
// Single entry point for all user actions
fun onIntent(intent: UserProfileIntent) {
when (intent) {
is UserProfileIntent.LoadProfile -> loadProfile(intent.userId)
is UserProfileIntent.EnableEditMode -> enableEditMode()
is UserProfileIntent.SaveProfile -> saveProfile(intent.name, intent.bio)
is UserProfileIntent.CancelEdit -> cancelEdit()
}
}
private fun loadProfile(userId: String) {
viewModelScope.launch {
// Transition to loading state
state.update { it.copy(isLoading = true, error = null) }
try {
val user = userRepository.getUser(userId)
// Single atomic state update
state.update {
it.copy(
user = user,
isLoading = false,
error = null
)
}
analyticsTracker.trackProfileViewed(userId)
} catch (e: Exception) {
// Error state is part of the main state
state.update {
it.copy(
isLoading = false,
error = "Unable to load profile"
)
}
}
}
}
private fun enableEditMode() {
// Simple, predictable update
state.update { it.copy(isEditing = true) }
}
private fun saveProfile(name: String, bio: String) {
viewModelScope.launch {
val currentUser = state.value.user ?: return@launch
state.update { it.copy(isLoading = true) }
try {
val updatedUser = userRepository.updateUser(
currentUser.id,
name = name,
bio = bio
)
state.update {
it.copy(
user = updatedUser,
isEditing = false,
isLoading = false
)
}
// Side effect to notify the user
_sideEffect.send(UserProfileSideEffect.ShowSuccess("Profile updated"))
} catch (e: Exception) {
state.update {
it.copy(isLoading = false, error = "Failed to save profile")
}
}
}
}
private fun cancelEdit() {
state.update { it.copy(isEditing = false) }
}
}
// All possible actions, explicitly typed
sealed class UserProfileIntent {
data class LoadProfile(val userId: String) : UserProfileIntent()
data object EnableEditMode : UserProfileIntent()
data class SaveProfile(val name: String, val bio: String) : UserProfileIntent()
data object CancelEdit : UserProfileIntent()
}
// Single, complete screen state
data class UserProfileState(
val user: User? = null,
val isLoading: Boolean = false,
val isEditing: Boolean = false,
val error: String? = null
)
// One-shot side effects
sealed class UserProfileSideEffect {
data class ShowSuccess(val message: String) : UserProfileSideEffect()
data class NavigateTo(val destination: String) : UserProfileSideEffect()
}A diferença é clara: um único fluxo de estado, ações explícitas e uma separação limpa entre estado persistente e efeitos de único uso.
Com o MVI, é possível registrar cada Intent e cada transição de estado. Reproduzir um bug torna-se trivial: basta reproduzir a sequência de Intents.
MVI com Jetpack Compose
O MVI brilha especialmente com o Jetpack Compose, pois ambos compartilham a mesma filosofia: estado imutável e interface declarativa. Veja como conectar o ViewModel a uma tela Compose:
// Compose screen consuming MVI state
// The connection between ViewModel and UI is elegant and reactive
@Composable
fun UserProfileScreen(
viewModel: UserProfileMviViewModel = hiltViewModel(),
onNavigateBack: () -> Unit
) {
// Collect the single state
val state by viewModel.state.collectAsStateWithLifecycle()
// Handle side effects
LaunchedEffect(Unit) {
viewModel.sideEffect.collect { effect ->
when (effect) {
is UserProfileSideEffect.ShowSuccess -> {
// Show snackbar
}
is UserProfileSideEffect.NavigateTo -> {
// Navigate
}
}
}
}
// Purely declarative UI based on state
UserProfileContent(
state = state,
onIntent = viewModel::onIntent
)
}
@Composable
private fun UserProfileContent(
state: UserProfileState,
onIntent: (UserProfileIntent) -> Unit
) {
Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
// Conditional rendering based on the single state
when {
state.isLoading -> {
CircularProgressIndicator(
modifier = Modifier.align(Alignment.CenterHorizontally)
)
}
state.error != null -> {
ErrorMessage(
message = state.error,
onRetry = {
state.user?.id?.let {
onIntent(UserProfileIntent.LoadProfile(it))
}
}
)
}
state.user != null -> {
ProfileCard(
user = state.user,
isEditing = state.isEditing,
onEditClick = { onIntent(UserProfileIntent.EnableEditMode) },
onSaveClick = { name, bio ->
onIntent(UserProfileIntent.SaveProfile(name, bio))
},
onCancelClick = { onIntent(UserProfileIntent.CancelEdit) }
)
}
}
}
}A interface se torna uma função pura do estado: previsível, testável e sem efeitos colaterais ocultos.
Comparação Detalhada
Agora que ambos os padrões estão claros, convém compará-los nos critérios que importam em produção.
Gerenciamento de Estado
A diferença fundamental reside no gerenciamento de estado. Essa distinção impacta diretamente a manutenibilidade a longo prazo.
// MVVM: potentially fragmented state
class MvvmViewModel : ViewModel() {
// Multiple sources of truth - manual synchronization needed
val users: StateFlow<List<User>>
field = MutableStateFlow(emptyList())
val selectedUser: StateFlow<User?>
field = MutableStateFlow(null)
val isLoading: StateFlow<Boolean>
field = MutableStateFlow(false)
val searchQuery: StateFlow<String>
field = MutableStateFlow("")
// What happens if selectedUser points to a user
// that's no longer in users after a refresh?
// -> Inconsistent state that's hard to detect
}
// MVI: consistent state by construction
class MviViewModel : ViewModel() {
// Single source of truth - inconsistencies are impossible
val state: StateFlow<UsersState>
field = MutableStateFlow(UsersState())
data class UsersState(
val users: List<User> = emptyList(),
val selectedUser: User? = null, // Always consistent with users
val isLoading: Boolean = false,
val searchQuery: String = ""
)
// Each update automatically maintains invariants
private fun selectUser(userId: String) {
state.update { currentState ->
currentState.copy(
selectedUser = currentState.users.find { it.id == userId }
)
}
}
}No MVVM, estados inconsistentes frequentemente se manifestam como bugs intermitentes difíceis de reproduzir. No MVI, se o estado é inválido, ele é deterministicamente inválido.
Testabilidade da Arquitetura
Ambas as arquiteturas são testáveis, mas o MVI oferece uma vantagem significativa graças à sua previsibilidade.
// MVVM test: requires verifying multiple flows
@Test
fun `loadUsers should update state correctly`() = runTest {
val viewModel = MvvmViewModel(fakeRepository)
// Observe multiple flows simultaneously
val users = mutableListOf<List<User>>()
val loadingStates = mutableListOf<Boolean>()
val job1 = launch { viewModel.users.toList(users) }
val job2 = launch { viewModel.isLoading.toList(loadingStates) }
viewModel.loadUsers()
advanceUntilIdle()
// Assertions on different flows
assertThat(users.last()).isEqualTo(expectedUsers)
assertThat(loadingStates).containsExactly(false, true, false)
job1.cancel()
job2.cancel()
}
// MVI test: single flow to verify, clear state sequence
@Test
fun `LoadUsers intent should produce correct state sequence`() = runTest {
val viewModel = MviViewModel(fakeRepository)
// Collect all states in order
val states = mutableListOf<UsersState>()
val job = launch { viewModel.state.toList(states) }
// Send the intent
viewModel.onIntent(UsersIntent.LoadUsers)
advanceUntilIdle()
// Verify the exact state sequence
assertThat(states).containsExactly(
UsersState(), // Initial
UsersState(isLoading = true), // Loading
UsersState(users = expectedUsers, isLoading = false) // Success
)
job.cancel()
}O MVI permite verificar a sequência exata de transições de estado, o que é especialmente útil em telas complexas com muitas interações.
Complexidade e Boilerplate
Desde o Kotlin 2.4, a diferença de boilerplate entre MVVM e MVI foi reduzida significativamente. Os explicit backing fields eliminam o padrão _state / state que antes adicionava linhas a cada ViewModel.
// MVVM: quick start, less code
class SimpleViewModel : ViewModel() {
val name: StateFlow<String>
field = MutableStateFlow("")
fun updateName(newName: String) {
name.value = newName
}
}
// Total: ~8 lines
// MVI: more structure, more code
class SimpleMviViewModel : ViewModel() {
val state: StateFlow<SimpleState>
field = MutableStateFlow(SimpleState())
fun onIntent(intent: SimpleIntent) {
when (intent) {
is SimpleIntent.UpdateName -> {
state.update { it.copy(name = intent.name) }
}
}
}
}
data class SimpleState(val name: String = "")
sealed class SimpleIntent {
data class UpdateName(val name: String) : SimpleIntent()
}
// Total: ~18 linesPara uma tela simples, o MVI pode parecer excessivo. Mas essa estrutura paga dividendos conforme a tela cresce em complexidade.
Pronto para mandar bem nas entrevistas de Android?
Pratique com nossos simuladores interativos, flashcards e testes tecnicos.
Quando Escolher MVVM?
O MVVM continua sendo a escolha pragmática em diversas situações:
Projetos Existentes
Se a aplicação já usa MVVM, migrar para MVI representa um esforço considerável. Melhorar a estrutura MVVM existente costuma ser a decisão mais sensata.
Equipes Júnior ou Mistas
O MVVM é mais acessível. Uma equipe com desenvolvedores iniciantes será produtiva mais rapidamente com MVVM do que com MVI.
Telas Simples
Para telas com poucos estados e interações, o MVI adiciona complexidade sem benefício proporcional.
// For a simple settings screen, MVVM is plenty
class SettingsViewModel(
private val preferencesRepository: PreferencesRepository
) : ViewModel() {
val darkMode = preferencesRepository.darkModeFlow
.stateIn(viewModelScope, SharingStarted.Lazily, false)
val notificationsEnabled = preferencesRepository.notificationsFlow
.stateIn(viewModelScope, SharingStarted.Lazily, true)
fun toggleDarkMode() {
viewModelScope.launch {
preferencesRepository.setDarkMode(!darkMode.value)
}
}
fun toggleNotifications() {
viewModelScope.launch {
preferencesRepository.setNotifications(!notificationsEnabled.value)
}
}
}Quando Escolher MVI?
O MVI demonstra seu valor em contextos específicos:
Aplicações com Estado Complexo
Quando uma tela tem muitos estados interdependentes, o MVI garante a consistência.
// Checkout screen with complex state: MVI excels
data class CheckoutState(
val cartItems: List<CartItem> = emptyList(),
val selectedAddress: Address? = null,
val selectedPayment: PaymentMethod? = null,
val promoCode: PromoCode? = null,
val deliveryOptions: List<DeliveryOption> = emptyList(),
val selectedDelivery: DeliveryOption? = null,
val subtotal: Money = Money.ZERO,
val discount: Money = Money.ZERO,
val deliveryFee: Money = Money.ZERO,
val total: Money = Money.ZERO,
val isLoading: Boolean = false,
val error: CheckoutError? = null,
val step: CheckoutStep = CheckoutStep.CART
) {
// Verifiable invariants
init {
require(total == subtotal - discount + deliveryFee) {
"Total inconsistent with components"
}
}
}
sealed class CheckoutIntent {
data class AddItem(val item: CartItem) : CheckoutIntent()
data class RemoveItem(val itemId: String) : CheckoutIntent()
data class SelectAddress(val address: Address) : CheckoutIntent()
data class SelectPayment(val method: PaymentMethod) : CheckoutIntent()
data class ApplyPromo(val code: String) : CheckoutIntent()
data object RemovePromo : CheckoutIntent()
data class SelectDelivery(val option: DeliveryOption) : CheckoutIntent()
data object ProceedToPayment : CheckoutIntent()
data object ConfirmOrder : CheckoutIntent()
}Aplicações em Tempo Real
Para apps com WebSockets, notificações push ou sincronização em tempo real, o MVI gerencia elegantemente múltiplos fluxos de dados.
Requisitos Rígidos de Depuração
Em domínios regulados (fintech, saúde), a capacidade de reproduzir exatamente uma sequência de eventos é inestimável.
O MVI facilita a implementação de "depuração com viagem no tempo": registrar todos os estados e reproduzir a sessão do usuário.
Alternativa Emergente: Circuit
Circuit, desenvolvido pelo Slack, oferece uma implementação MVI nativa do Compose que vale a pena considerar para novos projetos. Ele combina presenters com Molecule internamente, eliminando grande parte do boilerplate MVI enquanto mantém o fluxo de dados unidirecional.
O Circuit é particularmente interessante para projetos Kotlin Multiplatform, pois permite compartilhar a lógica de apresentação entre plataformas. No entanto, sua adoção fora do Slack ainda é limitada comparada à abordagem padrão com Jetpack ViewModel.
Abordagem Híbrida: O Melhor dos Dois Mundos
Na prática, muitas equipes adotam uma abordagem híbrida: MVI para telas complexas, MVVM simplificado para telas simples. Este é um padrão recomendado:
// Base ViewModel with lightweight MVI structure
// Reusable for all screens
abstract class MviViewModel<S, I>(initialState: S) : ViewModel() {
val state: StateFlow<S>
field = MutableStateFlow(initialState)
protected val currentState: S get() = state.value
// Single entry point for intents
abstract fun onIntent(intent: I)
// Helper to update state
protected fun updateState(reducer: S.() -> S) {
state.update { it.reducer() }
}
}
// Concrete implementation stays simple
class ProfileViewModel(
private val userRepository: UserRepository
) : MviViewModel<ProfileState, ProfileIntent>(ProfileState()) {
override fun onIntent(intent: ProfileIntent) {
when (intent) {
is ProfileIntent.Load -> load(intent.userId)
is ProfileIntent.Refresh -> refresh()
is ProfileIntent.ToggleFavorite -> toggleFavorite()
}
}
private fun load(userId: String) {
viewModelScope.launch {
updateState { copy(isLoading = true) }
val user = userRepository.getUser(userId)
updateState {
copy(user = user, isLoading = false)
}
}
}
private fun refresh() = load(currentState.user?.id ?: return)
private fun toggleFavorite() {
updateState {
copy(user = user?.copy(isFavorite = !user.isFavorite))
}
}
}Essa abordagem oferece os benefícios do MVI (único estado, intents tipados) sem boilerplate excessivo.
Recomendações
Estas são as recomendações para escolher entre as duas arquiteturas conforme o contexto:
Para Novos Projetos com Compose
Adotar MVI desde o início. Compose e MVI compartilham a mesma filosofia, e o investimento inicial se recupera rapidamente. Com os explicit backing fields do Kotlin 2.4, a diferença de boilerplate foi reduzida.
Para Projetos Existentes Baseados em Views
Manter MVVM, mas incorporar gradualmente as melhores práticas do MVI: estado único no ViewModel, ações tipadas com sealed classes.
Para Equipes Grandes
Padronizar em uma única abordagem e documentá-la. A consistência no código é mais importante do que a escolha do padrão em si.
O melhor padrão é aquele que a equipe compreende e aplica corretamente. Um MVVM bem implementado supera um MVI mal compreendido.
Sources
- Kotlin 2.4.0 What's New - Os explicit backing fields agora são estáveis, eliminando o boilerplate
_state / state - Jetpack Compose August 2026 Release - Compose 1.12 com BOM 2026.08.00
- Circuit by Slack - Arquitetura MVI nativa Compose com integração Molecule
- Android Architecture Guide - Recomendações oficiais do Google sobre arquitetura em camadas
Conclusão
MVVM e MVI são abordagens válidas para arquiteturar aplicações Android. O MVVM oferece simplicidade e familiaridade, enquanto o MVI traz previsibilidade e depuração mais fácil.
Lista de Verificação para a Decisão
- Escolher MVVM se: equipe júnior, projeto simples, migração custosa
- Escolher MVI se: Compose nativo, estado complexo, depuração crítica
- Abordagem híbrida recomendada: MVI leve com estado único, sem over-engineering
- Prioridade máxima: consistência em todo o código
Comece a praticar!
Teste seus conhecimentos com nossos simuladores de entrevista e testes tecnicos.
Independentemente da escolha, a chave está em compreender os pontos fortes e fracos de cada abordagem para tomar uma decisão embasada. O melhor código é aquele que a equipe consegue manter com tranquilidade a longo prazo.
Você saberia encontrar o bug em Android?
Um trecho real, um bug escondido, uma tentativa por dia. Sem conta para testar.

Escrito por
Anthony Fillion-MailletFundador da SharpSkill
Desenvolvedor fullstack há mais de 10 anos. Dirige a SharpSkill e responde por tudo o que é publicado aqui.
Atualizado em 19 de agosto de 2026
Tags
Compartilhar
Artigos relacionados

Modularização Android em 2026: Arquitetura Multi-Módulo e Perguntas de Entrevista
Guia completo sobre modularização Android em 2026. Aprenda a estruturar uma arquitetura multi-módulo, usar catálogos de versões Gradle e dominar as perguntas essenciais de entrevista técnica.

Jetpack Compose: Animações Avançadas Passo a Passo
Guia completo de animações avançadas no Compose: transições, AnimatedVisibility, Animatable, gestos e desempenho para interfaces Android fluidas.

As 20 perguntas mais frequentes sobre Jetpack Compose em entrevistas (2026)
As 20 perguntas de entrevista sobre Jetpack Compose mais comuns: recomposição, gerenciamento de estado, navegação, performance e padrões de arquitetura.