A arquitetura MVVM (Model-View-ViewModel) se consolidou como o padrao mais adotado no desenvolvimento Android moderno, especialmente quando combinada com Kotlin e os componentes do Jetpack. Diferente de abordagens mais antigas como MVC, o MVVM promove uma separacao clara de responsabilidades que resulta em codigo mais testavel, manutenivel e resiliente a mudancas de configuracao. Neste guia, vamos dissecar cada camada do MVVM, implementar exemplos praticos e explorar as melhores estrategias para projetos do mundo real.
O Que e a Arquitetura MVVM
O MVVM divide a aplicacao em tres camadas distintas. A camada Model representa os dados e a logica de negocios, incluindo repositorios, fontes de dados e entidades. A camada View e responsavel pela interface grafica – Activities, Fragments ou composables no Jetpack Compose. A camada ViewModel atua como intermediaria, expondo dados da Model para a View de forma reativa e gerenciando o estado da interface.
O fluxo de dados e unidirecional: a View observa o ViewModel, que por sua vez consulta a Model. A View nunca acessa a Model diretamente, e a Model nao conhece nem a View nem o ViewModel. Essa separacao facilita testes unitarios, pois cada camada pode ser testada de forma isolada.
Estrutura de Pastas Recomendada
Uma organizacao clara de pastas facilita a navegacao e manutencao do projeto:
// Estrutura de pacotes recomendada
// com.exemplo.app/
// data/
// model/ -> Entidades e data classes
// repository/ -> Repositorios
// remote/ -> API services (Retrofit)
// local/ -> DAOs e banco de dados (Room)
// ui/
// lista/ -> ListaFragment + ListaViewModel
// detalhe/ -> DetalheFragment + DetalheViewModel
// di/ -> Modulos de injecao de dependencia
// util/ -> Classes utilitarias
Essa organizacao por feature facilita o crescimento do projeto e torna claro onde cada componente se encontra.
A Camada Model
A Model encapsula os dados e as regras de negocio. Vamos criar um exemplo completo com uma entidade, um servico de API e um repositorio:
// Entidade de dominio
data class Tarefa(
val id: Long,
val titulo: String,
val descricao: String,
val concluida: Boolean = false,
val criadaEm: LocalDateTime = LocalDateTime.now()
)
// Interface do servico de API
interface TarefaApiService {
@GET("tarefas")
suspend fun listarTarefas(): List<TarefaDto>
@POST("tarefas")
suspend fun criarTarefa(@Body tarefa: TarefaDto): TarefaDto
@PUT("tarefas/{id}")
suspend fun atualizarTarefa(
@Path("id") id: Long,
@Body tarefa: TarefaDto
): TarefaDto
}
// Repository que unifica fontes de dados
class TarefaRepository(
private val apiService: TarefaApiService,
private val tarefaDao: TarefaDao
) {
fun observarTarefas(): Flow<List<Tarefa>> {
return tarefaDao.listarTodas().map { entidades ->
entidades.map { it.toDomain() }
}
}
suspend fun sincronizar() {
val tarefasRemotas = apiService.listarTarefas()
tarefaDao.inserirTodas(tarefasRemotas.map { it.toEntity() })
}
suspend fun criarTarefa(tarefa: Tarefa) {
val dto = tarefa.toDto()
val resposta = apiService.criarTarefa(dto)
tarefaDao.inserir(resposta.toEntity())
}
}
O Repository e o ponto central que decide de onde buscar os dados – cache local, rede ou ambos. Essa abstracao permite trocar a implementacao sem afetar o restante da aplicacao.
A Camada ViewModel
O ViewModel gerencia o estado da UI e sobrevive a mudancas de configuracao. Com Kotlin, podemos usar tanto LiveData quanto StateFlow:
class TarefaViewModel(
private val repository: TarefaRepository
) : ViewModel() {
// Estado da UI usando StateFlow
private val _uiState = MutableStateFlow<TarefaUiState>(TarefaUiState.Loading)
val uiState: StateFlow<TarefaUiState> = _uiState.asStateFlow()
// Evento unico (navegacao, snackbar, etc.)
private val _evento = Channel<TarefaEvento>()
val evento = _evento.receiveAsFlow()
init {
carregarTarefas()
}
fun carregarTarefas() {
viewModelScope.launch {
_uiState.value = TarefaUiState.Loading
try {
repository.sincronizar()
repository.observarTarefas().collect { tarefas ->
_uiState.value = TarefaUiState.Success(tarefas)
}
} catch (e: Exception) {
_uiState.value = TarefaUiState.Error(
e.message ?: "Erro desconhecido"
)
}
}
}
fun criarTarefa(titulo: String, descricao: String) {
viewModelScope.launch {
try {
val novaTarefa = Tarefa(
id = 0,
titulo = titulo,
descricao = descricao
)
repository.criarTarefa(novaTarefa)
_evento.send(TarefaEvento.TarefaCriada)
} catch (e: Exception) {
_evento.send(
TarefaEvento.Erro("Falha ao criar tarefa")
)
}
}
}
fun alternarConclusao(tarefa: Tarefa) {
viewModelScope.launch {
val atualizada = tarefa.copy(concluida = !tarefa.concluida)
repository.criarTarefa(atualizada)
}
}
}
// Sealed class para estados da UI
sealed class TarefaUiState {
object Loading : TarefaUiState()
data class Success(val tarefas: List<Tarefa>) : TarefaUiState()
data class Error(val mensagem: String) : TarefaUiState()
}
// Sealed class para eventos unicos
sealed class TarefaEvento {
object TarefaCriada : TarefaEvento()
data class Erro(val mensagem: String) : TarefaEvento()
}
A distincao entre estado (StateFlow) e evento (Channel) e fundamental. O estado pode ser re-observado a qualquer momento, enquanto eventos como navegacao ou mensagens de snackbar devem ser consumidos uma unica vez.
A Camada View
A View observa o ViewModel e reage as mudancas de estado:
class TarefaFragment : Fragment(R.layout.fragment_tarefa) {
private val viewModel: TarefaViewModel by viewModels()
private var _binding: FragmentTarefaBinding? = null
private val binding get() = _binding!!
private lateinit var adapter: TarefaAdapter
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
_binding = FragmentTarefaBinding.bind(view)
configurarRecyclerView()
observarEstado()
observarEventos()
binding.fabNovaTarefa.setOnClickListener {
mostrarDialogNovaTarefa()
}
}
private fun configurarRecyclerView() {
adapter = TarefaAdapter { tarefa ->
viewModel.alternarConclusao(tarefa)
}
binding.recyclerTarefas.adapter = adapter
binding.recyclerTarefas.layoutManager = LinearLayoutManager(requireContext())
}
private fun observarEstado() {
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { state ->
when (state) {
is TarefaUiState.Loading -> {
binding.progressBar.isVisible = true
binding.recyclerTarefas.isVisible = false
}
is TarefaUiState.Success -> {
binding.progressBar.isVisible = false
binding.recyclerTarefas.isVisible = true
adapter.submitList(state.tarefas)
}
is TarefaUiState.Error -> {
binding.progressBar.isVisible = false
Snackbar.make(
binding.root, state.mensagem,
Snackbar.LENGTH_LONG
).show()
}
}
}
}
}
}
private fun observarEventos() {
viewLifecycleOwner.lifecycleScope.launch {
viewModel.evento.collect { evento ->
when (evento) {
is TarefaEvento.TarefaCriada -> {
Snackbar.make(
binding.root, "Tarefa criada!",
Snackbar.LENGTH_SHORT
).show()
}
is TarefaEvento.Erro -> {
Snackbar.make(
binding.root, evento.mensagem,
Snackbar.LENGTH_LONG
).show()
}
}
}
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
O uso de repeatOnLifecycle garante que a coleta do Flow respeite o ciclo de vida do Fragment, evitando processamento desnecessario quando a tela nao esta visivel.
StateFlow vs LiveData
Ambos servem para expor estado reativo, mas possuem diferencas importantes. O LiveData e lifecycle-aware por padrao e foi projetado especificamente para Android. O StateFlow faz parte das Coroutines e e mais flexivel, podendo ser usado em camadas que nao dependem do Android SDK.
A recomendacao atual do Google e usar StateFlow em ViewModels modernos, especialmente em projetos que utilizam Jetpack Compose, onde a integracao com Flow e nativa.
Boas Praticas para MVVM com Kotlin
- Mantenha o ViewModel livre de referencias ao Android: nao passe Context, View ou Activity para o ViewModel. Use
AndroidViewModelapenas quando estritamente necessario. - Use sealed classes para estados: elas garantem que todos os estados possiveis sejam tratados no
when. - Separe estado de eventos: use StateFlow para estado e Channel ou SharedFlow para eventos unicos.
- Injete dependencias: nunca instancie repositorios diretamente no ViewModel. Use Hilt, Koin ou outra solucao de injecao.
- Evite logica de negocio na View: a View deve apenas observar e renderizar. Toda logica pertence ao ViewModel ou a camada de dominio.
- Teste o ViewModel isoladamente: com dependencias injetadas, basta criar mocks dos repositorios para testar toda a logica.
Erros Comuns e Armadilhas
- Coletar Flow sem respeitar o lifecycle: usar
lifecycleScope.launchdiretamente semrepeatOnLifecyclepode causar coleta em background desnecessaria e crashes. - ViewModel fazendo chamadas diretas a API: o ViewModel deve delegar ao Repository, nunca chamar servicos de rede diretamente.
- Estado mutavel exposto: expor
MutableStateFlowouMutableLiveDatapublicamente permite que a View altere o estado diretamente, quebrando o fluxo unidirecional. Sempre exponha a versao imutavel. - Excesso de ViewModels: nao crie um ViewModel para cada componente pequeno. Um ViewModel por tela geralmente e suficiente.
- Ignorar tratamento de erros: sempre encapsule chamadas assincronas em try-catch e comunique erros atraves do estado da UI.
Conclusao e Proximos Passos
A arquitetura MVVM com Kotlin proporciona uma base solida para aplicacoes Android de qualquer escala. A separacao entre Model, View e ViewModel resulta em codigo mais organizado, testavel e resistente a mudancas. Com o uso de StateFlow, sealed classes e coroutines, o Kotlin torna a implementacao do MVVM elegante e eficiente.
Para evoluir ainda mais, explore Clean Architecture para adicionar uma camada de dominio ao MVVM, estude Jetpack Compose para interfaces declarativas e implemente testes unitarios completos para seus ViewModels. Esses topicos possuem guias dedicados aqui no Kotlin Brasil e vao complementar o conhecimento adquirido neste artigo.