Como funciona a arquitetura MVVM no Android?
Resposta rápida: no Android moderno, MVVM separa a aplicação em Model, View e ViewModel. A tela em Jetpack Compose envia ações do usuário ao ViewModel, observa um StateFlow<UiState> com collectAsStateWithLifecycle() e renderiza esse estado. O ViewModel coordena a lógica de apresentação e conversa com um Repository, que decide como acessar API, Room ou cache. O fluxo recomendado é unidirecional: evento da UI → ViewModel → novo estado → UI.
Compose / View
| eventos do usuário
v
ViewModel -----> UiState (StateFlow)
|
v
Repository -----> API / Room / DataStore
Essa estrutura evita que uma Activity, um Fragment ou uma função @Composable concentre chamadas HTTP, regras de negócio, controle de carregamento e tratamento de erro. O resultado é uma tela mais fácil de testar, alterar e recompor sem perder estado.
Se você chegou aqui pesquisando “MVVM Android”, a implementação mais útil para projetos novos combina:
- Jetpack Compose para a View;
ViewModeldo AndroidX para manter o estado da tela;- StateFlow para expor estado observável;
- coroutines para trabalho assíncrono;
- Repository para esconder detalhes de rede e persistência;
- injeção de dependência com Hilt ou Koin;
- testes unitários do ViewModel e do Repository.
O que são Model, View e ViewModel?
Model: dados e regras da aplicação
A Model não é apenas uma data class. Ela representa os dados, as regras de negócio e os componentes que acessam fontes externas. Em um app real, essa parte pode incluir:
- entidades de domínio;
- repositories;
- clientes HTTP com Retrofit ou Ktor Client;
- banco local com Room;
- preferências com DataStore;
- casos de uso quando a regra merece uma classe própria.
O ponto importante é que a interface não deve saber se uma lista veio da internet, do banco local ou de um cache. Essa decisão pertence à camada de dados.
View: interface que renderiza estado
A View é a tela: uma função Compose, uma Activity ou um Fragment. Sua responsabilidade é pequena e objetiva:
- receber um estado;
- desenhar a interface;
- transformar interações em eventos;
- encaminhar esses eventos ao ViewModel.
A View não deveria calcular desconto, decidir estratégia de sincronização nem montar manualmente uma requisição de API. Em Compose, uma tela previsível recebe dados e callbacks, o que também facilita previews e testes.
ViewModel: estado e lógica de apresentação
O ViewModel prepara os dados para a tela e sobrevive a mudanças de configuração, como rotação. Ele deve:
- expor um estado imutável;
- processar ações da interface;
- iniciar operações assíncronas em
viewModelScope; - converter respostas e falhas em estados compreensíveis pela UI;
- depender de abstrações, não de componentes visuais.
Evite guardar Activity, Fragment, View, NavController ou Context no ViewModel. Quando um recurso Android for realmente necessário, prefira abstraí-lo atrás de uma interface ou usar APIs próprias para aquele problema.
Exemplo completo de MVVM com Kotlin, StateFlow e Compose
Vamos montar uma tela de tarefas. O objetivo não é criar uma arquitetura enorme, mas mostrar o caminho completo entre dados, ViewModel e UI.
1. Modele o domínio e o Repository
data class Tarefa(
val id: Long,
val titulo: String,
val concluida: Boolean,
)
interface TarefaRepository {
fun observarTarefas(): Flow<List<Tarefa>>
suspend fun atualizarConclusao(id: Long, concluida: Boolean)
suspend fun sincronizar()
}
A interface do Repository não menciona Retrofit, Room ou DTO. Isso permite usar uma implementação de produção e outra falsa nos testes.
Uma implementação offline-first poderia observar o banco local e sincronizar a API em segundo plano:
class TarefaRepositoryImpl(
private val dao: TarefaDao,
private val api: TarefaApi,
) : TarefaRepository {
override fun observarTarefas(): Flow<List<Tarefa>> =
dao.observarTodas().map { entidades ->
entidades.map(TarefaEntity::toDomain)
}
override suspend fun atualizarConclusao(
id: Long,
concluida: Boolean,
) {
dao.atualizarConclusao(id, concluida)
}
override suspend fun sincronizar() {
val remotas = api.listarTarefas()
dao.substituirTodas(remotas.map(TarefaDto::toEntity))
}
}
Para aprofundar esse desenho, leia o guia de Android offline-first com Kotlin.
2. Crie um UiState único
Uma única data class costuma ser mais prática que vários StateFlow independentes. Ela produz um retrato consistente da tela:
data class TarefasUiState(
val carregando: Boolean = true,
val tarefas: List<Tarefa> = emptyList(),
val mensagemErro: String? = null,
)
Com esse modelo, a UI não corre o risco de observar carregando = false em um fluxo e uma lista antiga em outro. Mudanças relacionadas são publicadas juntas.
3. Implemente o ViewModel
class TarefasViewModel(
private val repository: TarefaRepository,
) : ViewModel() {
val uiState: StateFlow<TarefasUiState> =
repository.observarTarefas()
.map<List<Tarefa>, TarefasUiState> { tarefas ->
TarefasUiState(
carregando = false,
tarefas = tarefas,
)
}
.catch { erro ->
emit(
TarefasUiState(
carregando = false,
mensagemErro = erro.message
?: "Não foi possível carregar as tarefas",
)
)
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = TarefasUiState(),
)
init {
sincronizar()
}
fun onConcluidaChange(tarefa: Tarefa, concluida: Boolean) {
viewModelScope.launch {
repository.atualizarConclusao(tarefa.id, concluida)
}
}
fun tentarNovamente() {
sincronizar()
}
private fun sincronizar() {
viewModelScope.launch {
runCatching { repository.sincronizar() }
.onFailure { erro ->
// Registre a falha em observabilidade.
// O banco local ainda pode manter a tela utilizável.
println("Falha na sincronização: ${erro.message}")
}
}
}
}
stateIn transforma o Flow do Repository em StateFlow, mantém um valor atual e controla quando o fluxo de origem permanece ativo. SharingStarted.WhileSubscribed(5_000) tolera pequenas interrupções de observação, como uma mudança de configuração, sem reiniciar o pipeline imediatamente.
Em uma aplicação com criação, busca e filtros, o ViewModel pode manter um MutableStateFlow privado para entradas da tela e combiná-lo com os dados do Repository. A regra permanece: exponha StateFlow, não MutableStateFlow.
4. Observe o estado no Compose
Separe a função que conversa com o ViewModel da função visual. A primeira coleta estado; a segunda recebe somente valores e callbacks.
@Composable
fun TarefasRoute(
viewModel: TarefasViewModel,
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
TarefasScreen(
uiState = uiState,
onConcluidaChange = viewModel::onConcluidaChange,
onTentarNovamente = viewModel::tentarNovamente,
)
}
@Composable
fun TarefasScreen(
uiState: TarefasUiState,
onConcluidaChange: (Tarefa, Boolean) -> Unit,
onTentarNovamente: () -> Unit,
) {
when {
uiState.carregando -> CircularProgressIndicator()
uiState.mensagemErro != null -> ErrorContent(
mensagem = uiState.mensagemErro,
onTentarNovamente = onTentarNovamente,
)
uiState.tarefas.isEmpty() -> Text("Nenhuma tarefa cadastrada")
else -> LazyColumn {
items(
items = uiState.tarefas,
key = Tarefa::id,
) { tarefa ->
Row {
Checkbox(
checked = tarefa.concluida,
onCheckedChange = { concluida ->
onConcluidaChange(tarefa, concluida)
},
)
Text(tarefa.titulo)
}
}
}
}
}
Use collectAsStateWithLifecycle() no Android em vez de collectAsState() quando o fluxo não precisa continuar sendo coletado com a tela fora do estado ativo. Isso integra a coleta ao lifecycle e reduz trabalho desnecessário.
Fluxo unidirecional de dados no MVVM
MVVM funciona melhor quando combinado com Unidirectional Data Flow (UDF):
- a UI mostra
UiState; - o usuário toca, digita ou atualiza a tela;
- a UI envia uma ação ao ViewModel;
- o ViewModel chama o domínio ou Repository;
- a fonte de dados muda;
- um novo
UiStatechega à UI.
Esse ciclo reduz estados impossíveis e facilita depuração. Em vez de a tela alterar diretamente uma lista e depois tentar sincronizá-la, a ação percorre um caminho conhecido.
Você pode representar ações com funções explícitas, como onConcluidaChange, ou com uma sealed interface quando a tela tem muitos eventos:
sealed interface TarefasAction {
data class AlterarConclusao(
val tarefa: Tarefa,
val concluida: Boolean,
) : TarefasAction
data object TentarNovamente : TarefasAction
}
Não crie uma classe de evento por hábito. Funções nomeadas são mais simples em telas pequenas; uma interface de ações fica útil quando o volume de interações cresce ou quando você quer registrar e testar a redução de eventos.
StateFlow ou LiveData no Android?
Para projetos novos com Kotlin e Compose, prefira StateFlow. Ele faz parte de coroutines, funciona fora do Android e combina com operadores como map, combine, debounce e stateIn.
| Critério | StateFlow | LiveData |
|---|---|---|
| Integração com coroutines | Nativa | Parcial |
| Valor inicial obrigatório | Sim | Não |
| Uso fora do Android | Sim | Não |
| Jetpack Compose | collectAsStateWithLifecycle() | observeAsState() |
| Projeto legado com XML | Funciona | Muito comum |
LiveData não está “errado”. Se um app baseado em Views já o usa de forma consistente, uma migração sem benefício claro pode apenas gerar trabalho. Em código novo, StateFlow normalmente oferece uma base mais flexível. Veja também o guia completo de Kotlin Flow.
Eventos únicos: navegação, Snackbar e mensagens
Um erro frequente em MVVM é tratar tudo como evento descartável. Mensagens importantes para o usuário podem se perder se forem emitidas enquanto a tela está em segundo plano.
Prefira transformar o resultado da operação em estado quando ele afetar a interface. Por exemplo, uma falha ao salvar pode aparecer em uiState.mensagemErro. Depois que a UI confirmar a exibição, envie uma ação para limpar a mensagem.
Para navegação, mantenha o NavController na camada de UI. O ViewModel pode expor que uma operação terminou; a rota decide para onde navegar. O guia de Navigation Compose com NavHost e NavController mostra essa separação na prática.
Como organizar pastas sem exagerar nas camadas
Uma estrutura por feature costuma escalar melhor que separar o projeto inteiro por tipo técnico:
feature/tarefas/
data/
TarefaRepositoryImpl.kt
TarefaApi.kt
TarefaDao.kt
domain/
Tarefa.kt
TarefaRepository.kt
presentation/
TarefasViewModel.kt
TarefasUiState.kt
TarefasScreen.kt
Em um app pequeno, domain/ pode ser desnecessário. Não crie UseCase, Mapper, Coordinator e interfaces duplicadas para uma operação trivial. Adicione uma camada quando ela isola uma regra, permite reutilização ou reduz acoplamento real.
MVVM também não substitui Clean Architecture. MVVM organiza a apresentação; Clean Architecture orienta a direção das dependências. É possível usar ambos, mas a arquitetura deve continuar proporcional ao produto.
Testando o ViewModel
Como o ViewModel depende de uma interface, o teste não precisa inicializar Retrofit, Room nem Android UI.
class FakeTarefaRepository : TarefaRepository {
private val tarefas = MutableStateFlow<List<Tarefa>>(emptyList())
override fun observarTarefas(): Flow<List<Tarefa>> = tarefas
override suspend fun atualizarConclusao(
id: Long,
concluida: Boolean,
) {
tarefas.update { lista ->
lista.map { tarefa ->
if (tarefa.id == id) tarefa.copy(concluida = concluida)
else tarefa
}
}
}
override suspend fun sincronizar() = Unit
fun emitir(vararg itens: Tarefa) {
tarefas.value = itens.toList()
}
}
@Test
fun `marcar tarefa atualiza o estado da tela`() = runTest {
val repository = FakeTarefaRepository()
val viewModel = TarefasViewModel(repository)
val tarefa = Tarefa(1, "Estudar StateFlow", false)
repository.emitir(tarefa)
viewModel.onConcluidaChange(tarefa, true)
advanceUntilIdle()
assertTrue(viewModel.uiState.value.tarefas.single().concluida)
}
Em projetos reais, configure o dispatcher principal de testes e use ferramentas como Turbine quando precisar validar sequências emitidas por Flow. O guia de testes de Flow e StateFlow com Turbine aprofunda esse cenário.
Erros comuns em arquitetura MVVM
Colocar regra de negócio no Composable
Um composable deve renderizar e encaminhar ações. Cálculos de domínio, chamadas HTTP e decisões de persistência dificultam testes e podem ser repetidos durante recomposições.
Expor MutableStateFlow
Isto permite que qualquer consumidor altere o estado:
val uiState = MutableStateFlow(TarefasUiState()) // evite expor assim
Mantenha mutabilidade privada ou derive o estado de fluxos do Repository.
Passar Context para o ViewModel
Guardar Activity ou Context pode causar vazamento e acoplamento. Para textos, a UI pode resolver resources; para serviços do sistema, use uma abstração injetada quando necessário.
Chamar Retrofit diretamente em cada tela
O ViewModel passa a conhecer DTO, política de cache e detalhes HTTP. Um Repository concentra essas decisões e permite evoluir para Room ou offline-first sem reescrever a tela.
Criar um estado booleano para cada detalhe
isLoading, hasError, isEmpty, isRefreshing e listas em fluxos separados podem gerar combinações contraditórias. Modele um UiState coerente e publique alterações relacionadas juntas.
Usar arquitetura como cerimônia
MVVM não exige dezenas de arquivos por tela. Uma tela simples pode ter Screen, ViewModel, UiState e um Repository compartilhado. Comece pequeno e extraia responsabilidades quando a complexidade aparecer.
Checklist de MVVM Android moderno
Antes de considerar a tela pronta, confirme:
- a View apenas renderiza estado e envia ações;
- o ViewModel não guarda referências a componentes visuais;
- o estado público é imutável;
- Compose coleta Flow com lifecycle;
- rede, banco e cache ficam atrás de um Repository;
- carregamento, conteúdo vazio e erro têm representação explícita;
- operações assíncronas usam
viewModelScope; - regras importantes têm testes unitários;
- navegação permanece na camada de UI;
- as camadas existem por necessidade, não por copiar um template.
Próximos passos
A melhor forma de aprender arquitetura MVVM com Kotlin é implementar uma feature completa: listar dados de uma API, armazenar em Room, expor StateFlow, renderizar no Compose e testar o ViewModel com um Repository falso.
Depois deste guia, avance nesta ordem:
- Jetpack Compose do básico ao avançado;
- Kotlin Flow na prática;
- Hilt no Android com módulos, scopes e testes;
- Clean Architecture com Kotlin;
- Android offline-first.
MVVM não é um objetivo por si só. Ele é uma forma de manter o fluxo da tela previsível, proteger regras e fontes de dados das mudanças de interface e tornar o app mais fácil de testar. Com ViewModel, StateFlow, Compose e um Repository bem definido, você já tem uma base sólida para a maioria das aplicações Android modernas.