Artigo · Blog
Você escolheu Kotlin Multiplatform. E o seu ViewModel virou um nó cego entre duas plataformas.
Kotlin Multiplatform promete compartilhar lógica. O que ninguém avisa é que compartilhar ViewModel entre Android e iOS muda o que o ViewModel precisa ser, e isso quebra os dois lados. Uma decisão que eu já vi custar semanas de refactor.
O que parecia uma decisão óbvia
Num projeto cross-platform com KMP, você tem uma camada de domínio compartilhada funcionando. UseCases, repositórios, modelos. Tudo compilando nos dois alvos. A tentação natural é subir um nível e compartilhar o ViewModel também.
Parece uma vitória. Menos código. Menos divergência entre plataformas. Uma fonte única de verdade para o estado da tela.
Eu já tomei essa decisão. E ela foi errada.
Não porque KMP seja ruim. Mas porque ViewModel, no Android e no iOS, não significa a mesma coisa. E quando você tenta fazer uma classe servir a dois contextos com ciclos de vida diferentes, você não compartilha código. Você cria uma terceira coisa que não pertence a nenhum dos dois mundos.
O problema que aparece cedo, mas é ignorado
No Android, ViewModel tem sobrevivência de configuração. Ele resiste à rotação de tela. Está ancorado ao ciclo de vida do Fragment ou da Activity. O StateFlow que ele expõe é consumido por collectAsStateWithLifecycle() no Compose, que respeita o estado ativo da UI.
No iOS, nada disso existe. Você vai expor esse ViewModel compartilhado como uma ObservableObject do SwiftUI, mas o modelo de notificação é diferente. Coroutines não têm equivalente direto. Flow precisa de uma ponte. Você vai usar Kotlinx.coroutines com um scope que precisa ser gerenciado manualmente porque não tem ViewModelScope no iOS.
O primeiro sinal de problema foi quando o scope do ViewModel compartilhado continuou vivo depois que a tela tinha sido descartada no iOS. Nenhum crash. Nenhum erro visível. Só uma chamada de rede rodando em background sem que ninguém estivesse ouvindo. Em produção, isso aparece como consumo de bateria anormal e chamadas duplicadas de API.
O que a arquitetura te esconde
A maioria dos artigos sobre KMP mostra a camada que funciona bem: dados, domínio, repositórios. A lógica de negócio pura é de fato portável. Não tem ciclo de vida. Não tem framework. Ela só transforma e valida.
O problema está na camada acima.
ViewModel não é lógica de negócio. Ele é o mediador entre o domínio e a UI. E mediação pressupõe conhecer os dois lados. Quando você tenta escrever esse mediador uma vez só para dois lados radicalmente diferentes, você acaba com um mediador que conhece nenhum dos dois direito.
Numa fintech onde trabalhei, o ViewModel compartilhado começou com doze propriedades de estado. Depois de três meses, tinha vinte e sete. Metade delas só fazia sentido em uma das plataformas. O iOS ignorava seis campos. O Android ignorava outros quatro. A classe comum tinha se tornado um acumulador de casos especiais de cada plataforma.
Isso tem um nome: acoplamento por conveniência. Você não compartilhou porque fazia sentido arquitetural. Você compartilhou porque era mais fácil escrever uma vez.
O que funciona na prática
A separação que se mostrou mais sustentável é manter o compartilhamento até o UseCase e parar ali.
shared/
domain/
model/
usecase/
repository/ (interface)
data/
repository/ (implementação)
android/
presentation/
viewmodel/ ← ViewModel Android nativo
ios/
presentation/
viewmodel/ ← ObservableObject SwiftUI nativo
Cada plataforma tem seu próprio ViewModel. Ele chama o UseCase compartilhado, converte o resultado para o estado que a UI daquela plataforma precisa, e gerencia seu próprio ciclo de vida com as ferramentas do ecossistema nativo.
No Android, viewModelScope e StateFlow. No iOS, Task gerenciado dentro do ObservableObject com @MainActor.
O UseCase compartilhado retorna um resultado puro: Result<DomainModel, DomainError>. Cada plataforma mapeia esse resultado para o seu próprio UiState.
Duplicação? Sim. Mas é a duplicação certa.
A duplicação que vale a pena
Existe uma crença na indústria de que duplicação é sempre tecnicamente inferior. Que DRY é uma lei, não uma heurística.
Na prática, a duplicação entre ViewModels de plataformas diferentes raramente é o problema real de manutenção. O problema real é quando a lógica de negócio diverge silenciosamente. Quando o Android calcula um saldo de uma forma e o iOS de outra porque cada time tocou na própria cópia ao longo do tempo.
Compartilhar UseCase resolve esse problema. É aqui que a divergência dói de verdade.
Compartilhar ViewModel não resolve nada equivalente. Só adiciona fricção nos dois times.
Num projeto com dois times trabalhando em paralelo, a equipe Android precisava adicionar uma lógica de retry com backoff exponencial no ViewModel de pagamento. O ViewModel era compartilhado. Qualquer mudança exigia validação no iOS antes de ser mergeada. O ciclo de review dobrou. A feature atrasou uma sprint por uma dependência que não deveria existir.
O trade-off honesto
Separar os ViewModels significa mais código. Significa que você vai escrever dois mapeamentos de DomainModel para UiState. Se o domínio mudar, os dois ViewModels precisam mudar.
Isso é real. Não é de graça.
Mas o custo alternativo é pior: um ViewModel que não pertence a nenhuma plataforma, que carrega bagagem das duas, que força coordenação entre times para qualquer mudança de apresentação, e que esconde bugs de ciclo de vida que aparecem em produção como sintomas difusos.
A promessa do KMP é reduzir divergência onde divergência machuca. Divergência de regra de negócio machuca. Divergência de apresentação é esperada. Plataformas diferentes têm UX diferente, têm padrões de navegação diferentes, têm convenções de estado diferentes.
Forçar convergência onde a natureza da plataforma pede divergência não é eficiência. É dívida técnica com aparência de arquitetura.
O que eu faria diferente
Eu definiria o contrato do compartilhamento antes de escrever qualquer linha de código.
A pergunta não é "o que eu posso compartilhar?" A pergunta certa é "onde a divergência entre plataformas é aceitável e onde ela é perigosa?"
Regras de negócio: perigoso divergir. Compartilha. Validação: perigoso divergir. Compartilha. Mapeamento de apresentação: divergência esperada. Não compartilha. Gerenciamento de ciclo de vida: divergência inevitável. Não tenta compartilhar.
Quando você responde essa pergunta antes de montar a estrutura de pastas, a arquitetura fica mais óbvia. E você para de compartilhar por conveniência e começa a compartilhar por intenção.
O que fica
KMP é uma das ferramentas multiplataforma mais sérias disponíveis hoje. A proposta é real: lógica de negócio compartilhada, plataformas nativas de cada lado.
Mas a palavra "compartilhado" virou um atalho mental perigoso. Você ouve "compartilhe a lógica" e começa a compartilhar tudo que parece lógica.
ViewModel parece lógica. Mas ele é estrutura. É o molde que conecta domínio a plataforma. E moldes não são universais.
O problema não é o KMP. É a expectativa de que compartilhar mais é sempre melhor.
Às vezes o código mais saudável que você pode escrever é aquele que conscientemente repete uma estrutura porque as duas plataformas merecem versões delas mesmas, não uma versão genérica que serve às duas pela metade.