Neste tutorial completo, você vai aprender a implementar a arquitetura MVVM (Model-View-ViewModel) no Android usando Kotlin. Vamos cobrir ViewModel, LiveData, StateFlow, o Repository pattern, injeção de dependência manual e construir um exemplo prático integrando Room e Retrofit. Ao final, você terá uma base sólida para estruturar seus projetos Android de forma escalável e testável.
O que é a Arquitetura MVVM?
O MVVM divide o código do aplicativo em três camadas com responsabilidades bem definidas:
- Model: camada de dados — inclui o banco de dados local (Room), APIs remotas (Retrofit), e a lógica de negócios
- View: camada de apresentação — Activities, Fragments e Composables que exibem dados e capturam interações do usuário
- ViewModel: camada intermediária — prepara e gerencia os dados que a View precisa exibir, sobrevivendo a mudanças de configuração (como rotação de tela)
O princípio fundamental é que a View observa o ViewModel, e o ViewModel não conhece a View. Esse desacoplamento facilita os testes unitários e mantém o código organizado conforme o projeto cresce.
Passo 1: Configurando as Dependências
Adicione as dependências no build.gradle.kts:
dependencies {
// ViewModel e LiveData
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0")
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.7.0")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0")
// Para coletar StateFlow de forma segura
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.7.0")
// Activity KTX (para viewModels() delegate)
implementation("androidx.activity:activity-ktx:1.8.2")
implementation("androidx.fragment:fragment-ktx:1.6.2")
// Coroutines
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
}
Passo 2: Criando o Model — Entity e DAO
Vamos construir um app de notas para ilustrar a arquitetura. Começamos pela camada de dados:
import androidx.room.*
import kotlinx.coroutines.flow.Flow
@Entity(tableName = "notas")
data class Nota(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
val titulo: String,
val conteudo: String,
val dataCriacao: Long = System.currentTimeMillis(),
val favorita: Boolean = false
)
@Dao
interface NotaDao {
@Query("SELECT * FROM notas ORDER BY dataCriacao DESC")
fun observarTodas(): Flow<List<Nota>>
@Query("SELECT * FROM notas WHERE favorita = 1 ORDER BY dataCriacao DESC")
fun observarFavoritas(): Flow<List<Nota>>
@Query("SELECT * FROM notas WHERE id = :id")
suspend fun buscarPorId(id: Long): Nota?
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun inserir(nota: Nota): Long
@Update
suspend fun atualizar(nota: Nota)
@Delete
suspend fun deletar(nota: Nota)
}
Passo 3: O Repository Pattern — Separando as Fontes de Dados
O Repository é a camada que abstrai as fontes de dados (local e remota) para o ViewModel. Ele decide de onde buscar os dados e como sincronizá-los:
class NotaRepository(
private val notaDao: NotaDao,
private val apiService: ApiService? = null // fonte remota opcional
) {
// Dados reativos do banco local
val todasNotas: Flow<List<Nota>> = notaDao.observarTodas()
val favoritas: Flow<List<Nota>> = notaDao.observarFavoritas()
suspend fun buscarPorId(id: Long): Nota? {
return notaDao.buscarPorId(id)
}
suspend fun salvar(nota: Nota): Long {
return notaDao.inserir(nota)
}
suspend fun atualizar(nota: Nota) {
notaDao.atualizar(nota)
}
suspend fun deletar(nota: Nota) {
notaDao.deletar(nota)
}
suspend fun alternarFavorita(nota: Nota) {
notaDao.atualizar(nota.copy(favorita = !nota.favorita))
}
// Exemplo de sincronização com API remota
suspend fun sincronizar(): Result<Unit> {
return try {
val notasRemotas = apiService?.buscarNotas() ?: return Result.success(Unit)
notaDao.inserirTodas(notasRemotas)
Result.success(Unit)
} catch (e: Exception) {
Result.failure(e)
}
}
}
O Repository recebe suas dependências pelo construtor, seguindo o princípio de inversão de dependência. Isso facilita a substituição por implementações de teste (mocks).
Passo 4: O ViewModel — Gerenciando o Estado da UI
O ViewModel é o coração da arquitetura MVVM. Ele expõe o estado da UI e processa ações do usuário:
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
// Estado da UI representado como uma data class imutável
data class NotasUiState(
val notas: List<Nota> = emptyList(),
val isLoading: Boolean = false,
val erro: String? = null,
val filtro: FiltroNotas = FiltroNotas.TODAS
)
enum class FiltroNotas { TODAS, FAVORITAS }
class NotaViewModel(
private val repository: NotaRepository
) : ViewModel() {
private val _uiState = MutableStateFlow(NotasUiState(isLoading = true))
val uiState: StateFlow<NotasUiState> = _uiState.asStateFlow()
private val _filtroAtual = MutableStateFlow(FiltroNotas.TODAS)
init {
observarNotas()
}
private fun observarNotas() {
viewModelScope.launch {
_filtroAtual.flatMapLatest { filtro ->
when (filtro) {
FiltroNotas.TODAS -> repository.todasNotas
FiltroNotas.FAVORITAS -> repository.favoritas
}
}.collect { notas ->
_uiState.update { state ->
state.copy(
notas = notas,
isLoading = false,
erro = null
)
}
}
}
}
fun alterarFiltro(filtro: FiltroNotas) {
_filtroAtual.value = filtro
_uiState.update { it.copy(filtro = filtro) }
}
fun adicionarNota(titulo: String, conteudo: String) {
viewModelScope.launch {
try {
repository.salvar(Nota(titulo = titulo, conteudo = conteudo))
} catch (e: Exception) {
_uiState.update { it.copy(erro = "Erro ao salvar nota: ${e.message}") }
}
}
}
fun deletarNota(nota: Nota) {
viewModelScope.launch {
try {
repository.deletar(nota)
} catch (e: Exception) {
_uiState.update { it.copy(erro = "Erro ao deletar nota") }
}
}
}
fun alternarFavorita(nota: Nota) {
viewModelScope.launch {
repository.alternarFavorita(nota)
}
}
fun limparErro() {
_uiState.update { it.copy(erro = null) }
}
}
O ViewModel usa viewModelScope para lançar coroutines que são automaticamente canceladas quando o ViewModel é destruído. O estado é exposto como StateFlow imutável para a View.
Passo 5: LiveData vs StateFlow no ViewModel
Tanto LiveData quanto StateFlow podem ser usados para expor estado. Aqui está a comparação:
// Abordagem com LiveData
class NotaViewModelComLiveData(
private val repository: NotaRepository
) : ViewModel() {
// LiveData é lifecycle-aware nativamente
val notas: LiveData<List<Nota>> = repository.todasNotas
.asLiveData() // converte Flow para LiveData
private val _erro = MutableLiveData<String?>()
val erro: LiveData<String?> = _erro
fun adicionarNota(titulo: String, conteudo: String) {
viewModelScope.launch {
try {
repository.salvar(Nota(titulo = titulo, conteudo = conteudo))
} catch (e: Exception) {
_erro.value = e.message
}
}
}
}
// Abordagem com StateFlow (recomendada para projetos novos)
class NotaViewModelComStateFlow(
private val repository: NotaRepository
) : ViewModel() {
val notas: StateFlow<List<Nota>> = repository.todasNotas
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = emptyList()
)
}
O SharingStarted.WhileSubscribed(5000) mantém o Flow ativo por 5 segundos após o último coletor se desconectar. Isso evita reiniciar a coleta durante rotações de tela, onde a Activity é destruída e recriada rapidamente.
Passo 6: A View — Conectando Tudo
Na Activity ou Fragment, observamos o ViewModel e atualizamos a UI:
import android.os.Bundle
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import kotlinx.coroutines.launch
class NotasActivity : AppCompatActivity() {
private val viewModel: NotaViewModel by viewModels {
NotaViewModelFactory(
(application as App).notaRepository
)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_notas)
// Coleta segura do StateFlow — respeita o lifecycle
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { state ->
atualizarUI(state)
}
}
}
}
private fun atualizarUI(state: NotasUiState) {
if (state.isLoading) {
// mostrar indicador de carregamento
} else {
// atualizar a lista no adapter
adapter.submitList(state.notas)
}
state.erro?.let { mensagem ->
// exibir Snackbar com o erro
Snackbar.make(rootView, mensagem, Snackbar.LENGTH_LONG).show()
viewModel.limparErro()
}
}
}
O repeatOnLifecycle garante que a coleta é pausada quando a Activity vai para o background e retomada quando volta. Isso evita atualizações desnecessárias e possíveis crashes.
Passo 7: Injeção de Dependência Manual
Sem bibliotecas como Hilt ou Koin, podemos fazer injeção de dependência manualmente usando a classe Application e ViewModelFactory:
class App : Application() {
// Lazy: instanciado apenas quando necessário
private val database by lazy { AppDatabase.getInstance(this) }
val notaRepository by lazy { NotaRepository(database.notaDao()) }
}
class NotaViewModelFactory(
private val repository: NotaRepository
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(NotaViewModel::class.java)) {
@Suppress("UNCHECKED_CAST")
return NotaViewModel(repository) as T
}
throw IllegalArgumentException("ViewModel desconhecido: ${modelClass.name}")
}
}
Esse padrão funciona bem para projetos pequenos e médios. Para projetos maiores, considere usar Hilt (recomendado pelo Google) ou Koin para gerenciar dependências automaticamente.
Passo 8: Exemplo Prático — Integrando Room e Retrofit
Vamos unir tudo em um Repository que busca dados da API e salva localmente:
class NotaRepository(
private val notaDao: NotaDao,
private val apiService: ApiService
) {
val notas: Flow<List<Nota>> = notaDao.observarTodas()
// Estratégia: mostrar dados locais e atualizar em background
suspend fun atualizarNotas(): Result<Unit> {
return try {
val notasRemotas = apiService.buscarNotas()
notaDao.inserirTodas(notasRemotas)
Result.success(Unit)
} catch (e: Exception) {
// Dados locais continuam disponíveis
Result.failure(e)
}
}
suspend fun criarNota(titulo: String, conteudo: String): Result<Nota> {
return try {
val nota = Nota(titulo = titulo, conteudo = conteudo)
// Salva localmente primeiro (offline-first)
val id = notaDao.inserir(nota)
val notaSalva = nota.copy(id = id)
// Tenta sincronizar com o servidor
try {
apiService.criarNota(notaSalva)
} catch (e: Exception) {
// Marca para sincronização futura
}
Result.success(notaSalva)
} catch (e: Exception) {
Result.failure(e)
}
}
}
No ViewModel, integramos tudo:
class NotaViewModel(private val repository: NotaRepository) : ViewModel() {
private val _uiState = MutableStateFlow(NotasUiState(isLoading = true))
val uiState: StateFlow<NotasUiState> = _uiState.asStateFlow()
init {
// Observa dados locais
viewModelScope.launch {
repository.notas.collect { lista ->
_uiState.update { it.copy(notas = lista, isLoading = false) }
}
}
// Atualiza do servidor em background
sincronizar()
}
fun sincronizar() {
viewModelScope.launch {
_uiState.update { it.copy(isLoading = true) }
repository.atualizarNotas().onFailure { e ->
_uiState.update { it.copy(erro = "Falha ao sincronizar: ${e.message}") }
}
_uiState.update { it.copy(isLoading = false) }
}
}
}
Erros Comuns
Colocar lógica de negócios na View: Activities e Fragments devem apenas observar o estado e delegar ações ao ViewModel. Qualquer lógica de processamento pertence ao ViewModel ou ao Repository.
ViewModel referenciando a View diretamente: O ViewModel nunca deve ter referência a Activities, Fragments ou Views. Isso causa memory leaks e quebra a testabilidade. Use StateFlow ou LiveData para comunicação.
Não usar
repeatOnLifecycle: Coletar StateFlow comlifecycleScope.launchsemrepeatOnLifecyclemantém a coleta ativa mesmo com o app em background, desperdiçando recursos.Estado mutável exposto publicamente: Sempre exponha
StateFlow(imutável) e mantenhaMutableStateFlowprivado. O mesmo vale paraLiveDatavsMutableLiveData.Criar o ViewModel manualmente: Nunca instancie ViewModels com
NotaViewModel(). UseviewModels()ouViewModelProviderpara que o Android gerencie o ciclo de vida corretamente.
Conclusão e Próximos Passos
Neste tutorial, você aprendeu a implementar a arquitetura MVVM com Kotlin de forma completa: separação de responsabilidades entre Model, View e ViewModel, uso de StateFlow e LiveData, o Repository pattern para abstração de dados, injeção de dependência manual, e um exemplo prático integrando Room e Retrofit.
Como próximos passos, recomendamos:
- Estudar Hilt para injeção de dependência automatizada e escalável
- Explorar Jetpack Compose para uma camada de View declarativa que se integra perfeitamente com o MVVM
- Aprender sobre Clean Architecture para projetos de grande escala
- Praticar testes unitários do ViewModel com
kotlinx-coroutines-test - Consultar o glossário de Flow e coroutine para reforçar conceitos
A arquitetura MVVM é o padrão recomendado pelo Google para apps Android e dominar sua implementação é essencial para escrever código limpo, escalável e testável.