---
title: "MVVM com Kotlin Tutorial em Português — Passo a Passo | Kotlin Brasil"
url: "https://kotlin.dev.br/tutoriais/kotlin-mvvm-tutorial/"
markdown_url: "https://kotlin.dev.br/tutoriais/kotlin-mvvm-tutorial.MD"
description: "Aprenda a arquitetura MVVM com Kotlin: ViewModel, LiveData, StateFlow, Repository pattern e injeção de dependência manual. Tutorial completo."
date: "2025-07-12"
author: "Karina Melo"
---

# MVVM com Kotlin Tutorial em Português — Passo a Passo | Kotlin Brasil

Aprenda a arquitetura MVVM com Kotlin: ViewModel, LiveData, StateFlow, Repository pattern e injeção de dependência manual. Tutorial completo.


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](/tutoriais/kotlin-room-database-tutorial/)), APIs remotas ([Retrofit](/tutoriais/kotlin-retrofit-tutorial/)), é 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**, é 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`:

```kotlin
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:

```kotlin
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:

```kotlin
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:

```kotlin
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](/glossario/coroutine/) que são automaticamente canceladas quando o ViewModel é destruído. O estado é exposto como [StateFlow](/glossario/flow/) 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:

```kotlin
// 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:

```kotlin
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:

```kotlin
class App : Application() {

    // Lazy: instanciado apenas quando necessario
    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:

```kotlin
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:

```kotlin
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

1. **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.

2. **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](/glossario/flow/) ou LiveData para comunicação.

3. **Não usar `repeatOnLifecycle`**: Coletar StateFlow com `lifecycleScope.launch` sem `repeatOnLifecycle` mantém a coleta ativa mesmo com o app em background, desperdiçando recursos.

4. **Estado mutável exposto publicamente**: Sempre exponha `StateFlow` (imutável) e mantenha `MutableStateFlow` privado. O mesmo vale para `LiveData` vs `MutableLiveData`.

5. **Criar o ViewModel manualmente**: Nunca instancie ViewModels com `NotaViewModel()`. Use `viewModels()` ou `ViewModelProvider` para 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 é 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](/tutoriais/kotlin-room-database-tutorial/) e [Retrofit](/tutoriais/kotlin-retrofit-tutorial/). Para preferências pequenas da tela, como filtros, ordenação e tema, complemente o repository com [DataStore Preferences](/tutoriais/datastore-preferences-kotlin/) em vez de criar tabelas desnecessárias.

Como próximos passos, recomendamos:

- Estudar Hilt para injeção de dependência automatizada e escalável
- Explorar [Jetpack Compose](/tutoriais/jetpack-compose-básico/) 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](/glossario/flow/) e [coroutine](/glossario/coroutine/) para reforçar conceitos
- Revisar [Android offline-first com Kotlin](/blog/android-offline-first-kotlin-2026/) para entender como MVVM, Room, DataStore e WorkManager trabalham juntos em apps reais

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. Padrões arquiteturais semelhantes são adotados em <a href="https://python.dev.br/" target="_blank" rel="noopener" onclick="umami.track('portfolio-site-click', { destination: 'python.dev.br' })">Python com Django e Flask</a> e <a href="https://golang.com.br/" target="_blank" rel="noopener" onclick="umami.track('portfolio-site-click', { destination: 'golang.com.br' })">Go com Clean Architecture</a>.
