MVVM (Model-View-ViewModel) é um padrão arquitetural no qual o ViewModel substitui o Presenter e usa mecanismos reativos para se comunicar com a View: ObservableObject no SwiftUI, LiveData/StateFlow no Android. O ViewModel não tem referência à View — os dados são passados por assinatura, eliminando a necessidade de interfaces ViewContract e tornando os testes ainda mais simples. A Apple recomenda MVVM com SwiftUI desde 2019, o Google recomenda MVVM com Jetpack como a arquitetura oficial do Android. Saiba mais em Android Architecture Guide.
Principais conclusões
MVVM (Model-View-ViewModel) é um padrão arquitetural descrito por John Gossman em 2005 para o Windows Presentation Foundation (WPF) da Microsoft. O ViewModel é o componente central que contém o estado da tela e a lógica de negócio, mas não tem referência à View. Os dados são transmitidos por meio de mecanismos de ligação reativa: a View assina as alterações do ViewModel e é renderizada automaticamente quando os dados mudam.
Diferença chave entre MVVM e MVP — ausência de ViewContract. No MVP, o Presenter chama métodos view.showUser(data), ou seja, o Presenter "empurra" ativamente os dados para a View. No MVVM, a própria View "puxa" os dados do ViewModel por meio de assinatura: o ViewModel não sabe se tem um assinante. Isso elimina o problema de View desconectada — se a Activity for destruída na rotação, o ViewModel continua funcionando e a nova Activity simplesmente assina os dados atuais. Na IT Sectr, usamos MVVM em todos os projetos novos desde 2020 — o código se tornou mais previsível, os testes mais estáveis.
| Componente | Responsabilidade | Plataforma |
|---|---|---|
| Model | Dados, lógica de negócio, repositórios | Android/iOS |
| View | Exibição, assinatura do ViewModel | Activity/Composable, UIView/SwiftUI View |
| ViewModel | Estado da tela, lógica, navegação | ViewModel (Jetpack), ObservableObject |
Ligação reativa — o fundamento do MVVM. No Android, o LiveData (parte do Jetpack) é um recipiente de dados observável. A Activity assina via observe(): viewModel.user.observe(this) { user -> binding.name.text = user.name }. Quando user muda, todos os assinantes recebem o novo valor automaticamente. No iOS, o SwiftUI usa propriedades @Published no ViewModel — as alterações renderizam automaticamente a View. Isso elimina as chamadas manuais showUser/hideLoading necessárias no MVP.
ViewModel do Jetpack — o componente oficial do Google para implementar MVVM. O ViewModel sobrevive à rotação de tela: quando a configuração muda, a Activity é destruída e recriada, enquanto o ViewModel permanece na memória. A nova instância da Activity obtém o mesmo ViewModel através do ViewModelProvider. O ViewModel não tem referências a Activity, Context ou View — é limpo e testável com testes unitários sem Robolectric.
// ViewModel com StateFlow — implementação moderna de MVVM
class UserViewModel(
private val repository: UserRepository
) : ViewModel() {
private val _state = MutableStateFlow<UserState>(UserState.Loading)
val state: StateFlow<UserState> = _state.asStateFlow()
fun loadUser(userId: Int) {
viewModelScope.launch {
_state.value = UserState.Loading
repository.getUser(userId)
.onSuccess { user ->
_state.value = UserState.Success(user)
}
.onFailure { e ->
_state.value = UserState.Error(e.message ?: "Unknown")
}
}
}
}
sealed interface UserState {
data object Loading : UserState
data class Success(val user: User) : UserState
data class Error(val message: String) : UserState
}
// View (Activity) assina state
class UserActivity : AppCompatActivity() {
private val viewModel: UserViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.state.onEach { state ->
when (state) {
is UserState.Loading -> /* mostrar carregamento */
is UserState.Success -> /* exibir dados */
is UserState.Error -> /* mostrar erro */
}
}.launchIn(lifecycleScope)
viewModel.loadUser(42)
}
}
LiveData vs StateFlow — LiveData (2017) — o primeiro componente reativo do Jetpack, otimizado para o ciclo de vida da Activity: cancelamento automático de assinatura no onStop. StateFlow (2021) — implementação Kotlin Flow, não vinculada ao ciclo de vida, mas que requer cancelamento manual via lifecycleScope. O StateFlow suporta corrotinas, concat, map e outros operadores Flow, que o LiveData não possui. Na IT Sectr, usamos StateFlow para todos os novos ViewModels — é mais curto, mais poderoso e se integra melhor com corrotinas.
DataBinding e ViewBinding — DataBinding liga o ViewModel ao XML via @{viewModel.user.name} diretamente no layout, eliminando código na Activity. ViewBinding gera uma classe type-safe para acessar as views. O Google recomenda ViewBinding para projetos simples e DataBinding para projetos com ligação de dados complexa. No Jetpack Compose, DataBinding não é necessário — funções @Composable são renderizadas automaticamente quando o State muda.
MVVM no iOS é implementado através do ObservableObject do Combine. O ViewModel é uma classe que herda ObservableObject, com propriedades @Published. A SwiftUI View assina o ViewModel via @ObservedObject ou @StateObject. Quando uma propriedade @Published muda, o SwiftUI renderiza automaticamente a View que depende dessa propriedade. A Apple apresentou o SwiftUI em 2019 na WWDC junto com o Combine — desde então o MVVM se tornou o padrão oficialmente recomendado para iOS.
import SwiftUI
import Combine
// ViewModel — ObservableObject com propriedades @Published
final class UserViewModel: ObservableObject {
@Published private(set) var state: UserState = .loading
private let service: UserService
init(service: UserService) {
self.service = service
}
func loadUser(id: Int) {
state = .loading
service.fetchUser(id: id) { [weak self] result in
guard let self else { return }
switch result {
case .success(let user):
self.state = .success(user)
case .failure(let error):
self.state = .error(error.localizedDescription)
}
}
}
}
enum UserState {
case loading
case success(User)
case error(String)
}
// SwiftUI View — assina ViewModel
struct UserView: View {
@StateObject private var viewModel: UserViewModel
var body: some View {
switch viewModel.state {
case .loading:
ProgressView()
case .success(let user):
VStack {
Text(user.name).font(.title)
Text(user.email).font(.body)
}
case .error(let message):
Text(message).foregroundColor(.red)
}
}
}
@StateObject vs @ObservedObject — @StateObject cria o ViewModel e gerencia seu ciclo de vida (uma vez por vida da View). @ObservedObject — o ViewModel é criado externamente e passado para a View. A WWDC 2022 recomenda @StateObject para criação e @ObservedObject para passar ViewModel entre Views. No iOS 17 (2023), surgiu a macro @Observable — automatizando a assinatura e eliminando as anotações @Published. @Observable é a evolução do Combine, aproximando o desenvolvimento iOS da reatividade do Kotlin Flow.
UIKit + MVVM — para projetos UIKit (sem SwiftUI), o MVVM é implementado através do Combine e @Published com assinatura no UIViewController via sink(). O ViewModel é o mesmo, a View é UIViewController com assinaturas em @Published. O Combine está disponível desde o iOS 13 (2019) e é embutido no sistema — não requer dependências adicionais. De acordo com a Apple Developer Survey (2025), 45% dos projetos iOS usam Combine mesmo com UIKit, 35% usam SwiftUI + Combine, 20% usam RxSwift (legado).
MVVM ganha do MVP em três aspectos principais: ausência de interfaces ViewContract, gerenciamento automático de assinaturas e sobrevivência à rotação de tela. No MVP, cada tela requer uma interface ViewContract + classe Presenter + assinatura/cancelamento em onStart/onStop. No MVVM, apenas o ViewModel é criado — a assinatura na Activity é feita via observe() sem detach() manual.
| Critério | MVP | MVVM |
|---|---|---|
| Interfaces ViewContract | 1 por tela | Não são necessárias |
| Gerenciamento de assinaturas | attach/detach manual | Automático (lifecycle-aware) |
| Rotação de tela | Retain-fragment | ViewModel sobrevive à rotação |
| Testes | Mock ViewContract | Classe limpa sem dependências |
| Reatividade | Callbacks no Presenter | LiveData/StateFlow/Combine |
Desvantagens do MVVM — complexidade de depurar cadeias reativas e risco de vazamento de memória com assinatura inadequada. LiveData resolve a segurança do ciclo de vida, StateFlow requer lifecycleScope, Combine requer sink com AnyCancellable. No MVP, todas as chamadas são explícitas (view.showUser), no MVVM os dados chegam através de um fluxo reativo — a rastreabilidade requer pontos de interrupção em closures subscribe. Em ViewModels grandes com vários StateFlows, você pode perder uma atualização de UI se a View não estiver assinada em um Flow específico.
Quando MVP ainda é melhor — em projetos com versão mínima do Android abaixo da API 21 (Android 5), onde o ViewModel do Jetpack não está disponível sem AndroidX, e em projetos usando UIKit puro sem Combine (iOS 12 e anteriores). Para projetos legados onde toda a base de código já está em MVP, a transição completa para MVVM nem sempre é justificada — é mais barato manter MVP com extração gradual da lógica em serviços do que reescrever 100 telas em 3 meses.
ViewModel é testado com testes unitários sem dependências de plataforma — este é o principal argumento a favor do MVVM. No Android, o ViewModel não contém Activity, Context ou View — todas as dependências (Repository, UseCase) são passadas pelo construtor e substituídas por objetos mock. No iOS, o ObservableObject é testado através do XCTest sem iniciar o aplicativo, proporcionando estabilidade e velocidade de execução dos testes.
// Teste unitário do ViewModel Android com MockK
class UserViewModelTest {
private val repository = mockk<UserRepository>()
private val viewModel = UserViewModel(repository)
@Test
fun loadUser_success_updatesState() = runTest {
val user = User(1, "John", "john@test.com")
coEvery { repository.getUser(1) } returns Result.success(user)
viewModel.loadUser(1)
assertEquals(UserState.Success(user), viewModel.state.value)
}
@Test
fun loadUser_error_updatesErrorState() = runTest {
val error = RuntimeException("Network error")
coEvery { repository.getUser(1) } returns Result.failure(error)
viewModel.loadUser(1)
val state = viewModel.state.value
assertTrue(state is UserState.Error)
assertEquals("Network error", (state as UserState.Error).message)
}
}
ViewModel do iOS é testado de forma semelhante: injetamos mock UserService, chamamos loadUser, verificamos o estado através de XCTestExpectation. O Combine Publisher é testado através de XCTestCase com wait(for: expectations, timeout: 1.0). A estrutura UserState — enum com valores associados — permite verificar o estado exato da tela após uma operação.
Cobertura de código em projetos da IT Sectr usando MVVM é de 75–90% para ViewModel e Repository. ViewModel é coberto por testes unitários, Repository por testes de integração com banco de dados de teste. View no SwiftUI e Jetpack Compose é testada com testes de UI (XCUITest, Compose Test) para cenários críticos. O restante da UI é verificado com testes de captura de tela (Snapshot Testing) — é mais rápido que testes de UI e fornece 95% de confiança na exibição correta.
Perguntas frequentes
No MVVM, o ViewModel não tem referência à View — os dados são transmitidos através de mecanismos reativos (LiveData, StateFlow, @Published). No MVP, o Presenter chama diretamente os métodos da View através da interface ViewContract. O MVVM elimina o ViewContract e o attach/detach manual, mas requer compreensão dos fluxos reativos. O ViewModel sobrevive à rotação de tela no Android, o Presenter requer um retain-fragment.
Conjunto mínimo: lifecycle-viewmodel-ktx (ViewModel), lifecycle-livedata-ktx ou kotlinx-coroutines-core (StateFlow). Para injeção — Hilt ou Koin. Para operações assíncronas — Kotlin Coroutines. Para ligação de dados complexa — DataBinding. No Jetpack Compose (recomendado pelo Google desde 2022), compose-runtime e lifecycle-viewmodel-compose são suficientes.
O SwiftUI (2019) é projetado para arquitetura reativa: @State e @Published renderizam automaticamente a View quando os dados mudam. MVVM é um ajuste natural para SwiftUI: View — @ViewBuilder, ViewModel — ObservableObject. A Apple não impõe MVVM como o único padrão, mas todos os materiais de treinamento desde 2019 usam ViewModel + SwiftUI. Para UIKit, a Apple recomenda MVC ou Coordinator.
Android: viewModelScope cancela automaticamente as corrotinas ao limpar o ViewModel. iOS: AnyCancellable do Combine cancela a assinatura automaticamente quando o objeto que o contém é desalocado. SwiftUI @StateObject gerencia o ciclo de vida automaticamente. Regras principais: não armazenar referências a View/Context no ViewModel, cancelar operações de longa duração na limpeza, usar weak self em closures.
StateFlow é a escolha moderna. LiveData é mais simples e seguro para o ciclo de vida, mas StateFlow é mais poderoso: funciona com corrotinas, suporta flatMap, combine, filter, não requer anotação @Nullable. O único cenário onde LiveData é preferível — trabalhar com código Java onde StateFlow (Kotlin Flow API) não está disponível. O Google recomenda StateFlow para novos projetos em Kotlin.
Resumo
Vamos desenvolver um aplicativo móvel chave na mão
A IT Sectr cria aplicativos para iOS e Android para startups e empresas desde 2017. Nós vamos aconselhá-lo e propor a melhor solução.
Leia também