31 de mai. de 2025·7 min de leitura

Kotlin MVI vs MVVM para apps Android com muitos formulários: estados de UI

Kotlin MVI vs MVVM para apps Android com muitos formulários, explicado com formas práticas de modelar validação, UI otimista, estados de erro e rascunhos offline.

Kotlin MVI vs MVVM para apps Android com muitos formulários: estados de UI

Por que apps Android com muitos formulários ficam complicados rápido

Apps com muitos formulários parecem lentos ou frágeis porque os usuários estão constantemente esperando decisões pequenas que seu código precisa tomar: este campo é válido, o salvamento funcionou, devemos mostrar um erro e o que acontece se a rede cair.

Formulários também expõem bugs de estado primeiro porque misturam vários tipos de estado ao mesmo tempo: estado da UI (o que é visível), estado de entrada (o que o usuário digitou), estado do servidor (o que está salvo) e estado temporário (o que está em andamento). Quando esses ficam fora de sincronia, o app começa a parecer “aleatório”: botões desativam na hora errada, erros antigos persistem ou a tela reinicia após rotação.

A maioria dos problemas se concentra em quatro áreas: validação (especialmente regras entre campos), UI otimista (feedback rápido enquanto o trabalho ainda está rodando), tratamento de erros (falhas claras e recuperáveis) e rascunhos offline (não perder trabalho inacabado).

Uma boa UX de formulário segue algumas regras simples:

  • A validação deve ser útil e próxima ao campo. Não bloqueie a digitação. Seja rigoroso quando importa, geralmente no envio.
  • A UI otimista deve refletir a ação do usuário imediatamente, mas também precisa de um rollback limpo se o servidor rejeitar.
  • Erros devem ser específicos, acionáveis e nunca apagar a entrada do usuário.
  • Rascunhos devem sobreviver a reinícios, interrupções e conexões ruins.

É por isso que debates de arquitetura ficam intensos para formulários. O padrão que você escolhe decide quão previsíveis esses estados se comportam sob pressão.

Rápido lembrete: MVVM e MVI em termos simples

A verdadeira diferença entre MVVM e MVI é como a mudança flui através de uma tela.

MVVM (Model View ViewModel) normalmente se parece com isto: o ViewModel mantém os dados da tela, os expõe para a UI (geralmente via StateFlow ou LiveData) e fornece métodos como save, validate ou load. A UI chama funções do ViewModel quando o usuário interage.

MVI (Model View Intent) normalmente funciona assim: a UI envia eventos (intents), um reducer os processa e a tela é renderizada a partir de um único objeto de estado que representa tudo o que a UI precisa naquele momento. Efeitos colaterais (rede, banco) são disparados de forma controlada e reportam resultados de volta como eventos.

Uma maneira simples de lembrar a mentalidade:

  • MVVM pergunta: “Quais dados o ViewModel deve expor e quais métodos ele deve oferecer?”
  • MVI pergunta: “Quais eventos podem acontecer e como eles transformam um estado em outro?”

Qualquer padrão funciona bem para telas simples. Quando você adiciona validação entre campos, autosave, retries e rascunhos offline, precisa de regras mais rígidas sobre quem pode mudar o estado e quando. MVI aplica essas regras por padrão. MVVM ainda pode funcionar bem, mas exige disciplina: caminhos de atualização consistentes e tratamento cuidadoso de eventos pontuais da UI (toasts, navegação).

Como modelar o estado do formulário sem surpresas

A maneira mais rápida de perder o controle é deixar os dados do formulário viverem em muitos lugares: bindings de view, múltiplos flows e “só mais um” booleano. Telas com muitos formulários se mantêm previsíveis quando há uma única fonte da verdade.

Um formato prático para FormState

Aposte em um único FormState que contenha entradas brutas mais algumas flags derivadas em que você pode confiar. Mantenha-o simples e completo, mesmo que pareça um pouco grande.

data class FormState(
  val fields: Fields,
  val fieldErrors: Map\u003cFieldId, String\u003e = emptyMap(),
  val formError: String? = null,
  val isDirty: Boolean = false,
  val isValid: Boolean = false,
  val submitStatus: SubmitStatus = SubmitStatus.Idle,
  val draftStatus: DraftStatus = DraftStatus.NotSaved
)

sealed class SubmitStatus { object Idle; object Saving; object Saved; data class Failed(val msg: String) }
sealed class DraftStatus { object NotSaved; object Saving; object Saved }

Isso mantém a validação ao nível do campo (por entrada) separada de problemas ao nível do formulário (como “total deve ser \u003e 0”). Flags derivadas como isDirty e isValid devem ser computadas em um lugar só, não reimplementadas na UI.

Um modelo mental limpo é: fields (o que o usuário digitou), validation (o que está errado), status (o que o app está fazendo), dirtiness (o que mudou desde o último save) e drafts (se existe uma cópia offline).

Onde efeitos pontuais pertencem

Formulários também disparam eventos de uma vez: snackbars, navegação, banners de “salvo”. Não coloque isso dentro do FormState, ou eles vão disparar de novo na rotação ou quando a UI se reinscrever.

Em MVVM, emita efeitos por um canal separado (por exemplo, um SharedFlow). Em MVI, modele-os como Effects (ou Events) que a UI consome uma vez. Essa separação previne erros “fantasmas” e mensagens de sucesso duplicadas.

Fluxo de validação em MVVM vs MVI

Validação é onde telas de formulário começam a ficar frágeis. A escolha chave é onde as regras vivem e como os resultados voltam para a UI.

Regras simples e síncronas (campos obrigatórios, tamanho mínimo, intervalos numéricos) devem rodar no ViewModel ou na camada de domínio, não na UI. Isso mantém as regras testáveis e consistentes.

Regras assíncronas (como “este email já existe?”) são mais complicadas. Você precisa lidar com loading, resultados obsoletos e o caso de “o usuário digitou de novo”.

No MVVM, a validação costuma virar uma mistura de estado e métodos auxiliares: a UI envia mudanças (atualizações de texto, perda de foco, cliques de submit) para o ViewModel; o ViewModel atualiza um StateFlow/LiveData e expõe erros por campo e um canSubmit derivado. Checks assíncronos geralmente iniciam um job, atualizam uma flag de loading e um erro quando terminam.

No MVI, a validação tende a ser mais explícita. Uma divisão prática de responsabilidades é:

  • O reducer executa validação síncrona e atualiza os erros de campo imediatamente.
  • Um effect roda validação assíncrona e despacha um intent de resultado.
  • O reducer aplica esse resultado apenas se ainda corresponder à entrada mais recente.

Essa última etapa importa. Se o usuário digita um novo email enquanto o check de “email único” está em andamento, resultados antigos não devem sobrescrever a entrada atual. MVI frequentemente torna isso mais fácil de codificar porque você pode armazenar o último valor verificado no estado e ignorar respostas obsoletas.

UI otimista e salvamentos assíncronos

Valide UX antes do código Kotlin
Rascunhe sua UI móvel e fluxo; gere apps nativos quando estiver pronto.
Criar app mobile

UI otimista significa que a tela se comporta como se o salvamento já tivesse acontecido antes da resposta da rede. Num formulário, isso costuma significar que o botão Salvar vira “Saving...”, um pequeno indicador “Salvo” aparece quando termina e os inputs continuam disponíveis (ou são intencionalmente travados) enquanto a requisição está em voo.

No MVVM, isso é comumente implementado alternando flags como isSaving, lastSavedAt e saveError. O risco é drift: salvamentos sobrepostos podem deixar essas flags inconsistentes. No MVI, um reducer atualiza um único objeto de estado, então Saving e Disabled têm menos chance de contradizer um ao outro.

Para evitar double submit e condições de corrida, trate cada salvamento como um evento identificado. Se o usuário tocar em Salvar duas vezes ou editar durante um save, você precisa de uma regra sobre qual resposta vence. Alguns salvaguardas funcionam em qualquer padrão: desative Salvar enquanto estiver salvando (ou debouce cliques), anexe um requestId (ou versão) a cada save e ignore respostas obsoletas, cancele trabalho em voo quando o usuário sair e defina o que edições significam durante o salvamento (enfileirar outro salvamento ou marcar o formulário como sujo novamente).

Sucesso parcial também é comum: o servidor aceita alguns campos e rejeita outros. Modele isso explicitamente. Mantenha erros por campo (e, se necessário, status de sincronização por campo) para que você possa mostrar “Salvo” no geral enquanto ainda destaca um campo que precisa de atenção.

Estados de erro que o usuário pode recuperar

Mantenha usuários informados automaticamente
Envie confirmações e alertas por email, SMS ou Telegram a partir da lógica do processo.
Adicionar mensagens

Telas de formulário falham de mais maneiras do que “algo deu errado”. Se toda falha virar um toast genérico, usuários reescrevem dados, perdem confiança e abandonam o fluxo. O objetivo é sempre o mesmo: manter a entrada segura, mostrar uma correção clara e tornar a nova tentativa natural.

Ajuda separar erros pelo local onde pertencem. Um formato errado não é o mesmo que uma queda de servidor.

Erros de campo devem ser inline e ligados a um input. Erros ao nível do formulário devem ficar perto da ação de envio e explicar o que bloqueia o envio. Erros de rede devem oferecer retry e manter o formulário editável. Erros de permissão ou autenticação devem guiar o usuário a reautenticar preservando o rascunho.

Uma regra central de recuperação: nunca limpe a entrada do usuário na falha. Se o salvamento falhar, mantenha os valores atuais em memória e em disco. A tentativa de retry deve reenviar o mesmo payload, a menos que o usuário edite.

Onde os padrões diferem é em como erros do servidor são mapeados de volta para o estado da UI. No MVVM, é fácil atualizar múltiplos flows ou campos e acidentalmente criar inconsistências. No MVI, você normalmente aplica a resposta do servidor em um único passo do reducer que atualiza fieldErrors e formError juntos.

Também decida o que é estado versus um efeito pontual. Erros inline e “falha no envio” pertencem ao estado (devem sobreviver à rotação). Ações pontuais como snackbar, vibração ou navegação devem ser efeitos.

Rascunhos offline e restauração de formulários em andamento

Um app com muitos formulários parece “offline” mesmo quando a rede está ok. Usuários trocam de app, o SO mata seu processo ou perdem sinal no meio. Rascunhos os impedem de começar de novo.

Primeiro, defina o que significa um rascunho. Salvar apenas o modelo “limpo” frequentemente não é suficiente. Normalmente você quer restaurar a tela exatamente como estava, incluindo campos meio digitados.

O que vale a pena persistir é, em geral, a entrada bruta do usuário (strings como digitadas, IDs selecionados, URIs de anexos), mais metadados suficientes para mesclar com segurança depois: um snapshot conhecido do servidor e um marcador de versão (updatedAt, ETag ou um simples incremento). A validação pode ser recomputada na restauração.

A escolha de armazenamento depende de sensibilidade e tamanho. Pequenos rascunhos podem viver em preferences, mas forms multi-step e anexos são mais seguros num banco local. Se o rascunho contém dados pessoais, use armazenamento criptografado.

A maior questão de arquitetura é onde fica a fonte da verdade. No MVVM, times frequentemente persistem a partir do ViewModel sempre que campos mudam. No MVI, persistir após cada atualização do reducer pode ser mais simples porque você está salvando um estado coerente (ou um objeto Draft derivado).

Temporização do autosave importa. Salvar a cada tecla é ruidoso; um debounce curto (por exemplo, 300 a 800 ms) mais um save na mudança de etapa funciona bem.

Quando o usuário ficar online de novo, você precisa de regras de merge. Uma abordagem prática é: se a versão do servidor não mudou, aplique o rascunho e submeta. Se mudou, mostre uma escolha clara: manter meu rascunho ou recarregar os dados do servidor.

Passo a passo: implementar um formulário confiável com qualquer padrão

Vá do formulário ao pagamento
Se seu formulário termina em pagamento, adicione a lógica do Stripe no mesmo fluxo.
Conectar Stripe

Formulários confiáveis começam com regras claras, não código de UI. Toda ação do usuário deve levar a um estado previsível, e todo resultado assíncrono deve ter um lugar óbvio para pousar.

Escreva as ações que sua tela precisa reagir: digitar, perder foco, enviar, retry e navegação entre passos. Em MVVM isso vira métodos do ViewModel e updates de estado. Em MVI viram intents explícitos.

Depois, construa em pequenos passos:

  1. Defina eventos para o ciclo de vida completo: edit, blur, submit, save success/failure, retry, restore draft.
  2. Projete um objeto de estado único: valores de campo, erros por campo, status geral do formulário e “tem mudanças não salvas”.
  3. Adicione validação: checagens leves durante edição, checagens mais pesadas no submit.
  4. Adicione regras de save otimista: o que muda imediatamente e o que aciona rollback.
  5. Adicione rascunhos: autosave com debounce, restaure na abertura e mostre um pequeno indicador “rascunho restaurado” para que usuários confiem no que veem.

Trate erros como parte da experiência. Mantenha a entrada, destaque só o que precisa ser corrigido e ofereça uma ação clara (editar, tentar de novo, ou manter rascunho).

Se quiser prototipar estados complexos de formulários antes de escrever UI Android, uma plataforma no-code como AppMaster pode ser útil para validar o fluxo primeiro. Depois você pode implementar as mesmas regras em MVVM ou MVI com menos surpresas.

Exemplo: formulário de relatório de despesas em vários passos

Imagine um formulário de despesas em 4 passos: detalhes (data, categoria, valor), upload de recibo, notas, depois revisar e enviar. Após o envio, mostra um status de aprovação como Draft, Submitted, Rejected, Approved. As partes complicadas são validação, salvamentos que podem falhar e manter rascunho quando o telefone fica offline.

No MVVM, você normalmente mantém um FormUiState no ViewModel (geralmente um StateFlow). Cada mudança de campo chama uma função do ViewModel como onAmountChanged() ou onReceiptSelected(). Validação roda na mudança, na navegação entre passos ou no submit. Uma estrutura comum é entradas brutas mais erros por campo, com flags derivadas controlando se Next/Submit estão habilitados.

No MVI, o mesmo fluxo vira explícito: a UI envia intents como AmountChanged, NextClicked, SubmitClicked e RetrySave. Um reducer retorna um novo estado. Efeitos colaterais (upload de recibo, chamada de API, mostrar um snackbar) rodam fora do reducer e alimentam resultados de volta como eventos.

Na prática, MVVM facilita adicionar funções e atualizar um flow rapidamente. MVI dificulta pular transições de estado acidentalmente porque toda mudança é canalizada pelo reducer.

Erros e armadilhas comuns

Construa em torno de um único modelo
Projete uma única fonte da verdade com o Data Designer e mantenha o estado do app consistente.
Criar projeto

A maioria dos bugs de formulário vem de regras pouco claras sobre quem é dono da verdade, quando a validação roda e o que acontece quando resultados assíncronos chegam fora de ordem.

O erro mais comum é misturar fontes da verdade. Se um campo de texto às vezes lê de um widget, às vezes do ViewModel e às vezes de um rascunho restaurado, você terá resets aleatórios e relatos de “minha entrada desapareceu”. Escolha um estado canônico para a tela e derive todo o resto dele (modelo de domínio, linhas de cache, payloads de API).

Outra armadilha fácil é confundir estado com eventos. Um toast, navegação ou banner “Salvo!” é pontual. Uma mensagem de erro que deve permanecer até o usuário editar é estado. Misturar isso causa efeitos duplicados na rotação ou feedbacks ausentes.

Dois problemas de corretude aparecem frequentemente:

  • Validar demais a cada tecla, especialmente para checagens caras. Debounce, valide no blur ou valide apenas campos tocados.
  • Ignorar resultados assíncronos fora de ordem. Se o usuário salva duas vezes ou edita depois do salvamento, respostas antigas podem sobrescrever entradas mais novas a menos que você use request IDs (ou lógica de “apenas o mais recente”).

Por fim, rascunhos não são “apenas salvar JSON”. Sem versionamento, atualizações do app podem quebrar restaurações. Adicione uma versão de esquema simples e uma história de migração, mesmo que seja “descartar e começar de novo” para rascunhos muito antigos.

Checklist rápido antes de lançar

Torne mudanças de estado previsíveis
Transforme suas regras de validação e salvamento em Processos de Negócio testáveis desde cedo.
Comece a construir

Antes de discutir MVVM vs MVI, certifique-se de que seu formulário tem uma fonte clara da verdade. Se um valor pode mudar na tela, ele pertence ao estado, não a um widget de view ou a uma flag escondida.

Uma verificação prática antes do envio:

  • O estado inclui inputs, erros por campo, status de salvamento (idle/saving/saved/failed) e status de rascunho/fila para que a UI nunca precise adivinhar.
  • Regras de validação são puras e testáveis sem UI.
  • UI otimista tem um caminho de rollback para rejeição do servidor.
  • Erros nunca apagam a entrada do usuário.
  • Restauração de rascunho é previsível: ou um banner claro de auto-restore ou uma ação explícita “Restaurar rascunho”.

Um teste que pega bugs reais: ligue o modo avião no meio de um salvamento, desligue e tente novamente duas vezes. A segunda tentativa não deve criar duplicatas. Use um request ID, chave de idempotência ou um marcador local de “salvamento pendente” para que retries sejam seguros.

Se suas respostas estão vagas, refine primeiro o modelo de estado e então escolha o padrão que torna essas regras mais fáceis de fazer cumprir.

Próximos passos: escolher um caminho e desenvolver mais rápido

Comece com uma pergunta: quão custoso é se seu formulário ficar em um estado meio atualizados? Se o custo é baixo, mantenha simples.

MVVM é uma boa escolha quando a tela é direta, o estado é basicamente “campos + erros” e sua equipe já entrega com confiança usando ViewModel + LiveData/StateFlow.

MVI é melhor quando você precisa de transições de estado estritas, muitos eventos assíncronos (autosave, retry, sync) ou quando bugs são caros (pagamentos, compliance, fluxos críticos).

Qualquer caminho que escolher, os testes de maior retorno para formulários geralmente não tocam UI: casos extremos de validação, transições de estado (editar, enviar, sucesso, falha, retry), rollback de save otimista e restauração/mesclagem de rascunho.

Se você também precisa do backend, telas admin e APIs junto com seu app móvel, AppMaster (appmaster.io) pode gerar backend pronto para produção, web e apps nativos a partir de um único modelo, o que ajuda a manter regras de validação e fluxo consistentes entre superfícies.

FAQ

Quando devo escolher MVVM ou MVI para uma tela Android com muitos formulários?

Escolha MVVM quando o fluxo do formulário for principalmente linear e sua equipe já tiver convenções sólidas para StateFlow/LiveData, eventos pontuais e cancelamento. Escolha MVI quando você espera muito trabalho assíncrono sobreposto (autosave, tentativas, uploads) e precisa de regras estritas para evitar que mudanças de estado “entrem sorrateiramente” por vários caminhos.

Qual a maneira mais simples de evitar que o estado do formulário saia de sincronia?

Comece com um único objeto de estado para a tela (por exemplo, FormState) que contenha valores brutos dos campos, erros por campo, um erro ao nível do formulário e status claros como Saving ou Failed. Mantenha flags derivadas como isValid e canSubmit computadas em um só lugar para que a UI apenas renderize, sem redecidir lógica.

Com que frequência devo rodar validação num formulário: a cada tecla ou apenas no envio?

Faça verificações leves e baratas enquanto o usuário digita (obrigatório, faixa, formato básico) e execute checagens mais rigorosas no submit. Mantenha o código de validação fora da UI para que seja testável e armazene erros no estado para que sobrevivam a rotações e restaurações de processo.

Como lidar com validação assíncrona como “email já está em uso” sem resultados defasados?

Trate validação assíncrona como “o último input vence”. Armazene o valor que foi validado (ou um id de requisição/versão) e ignore resultados que não correspondam ao estado atual. Isso evita que respostas antigas sobrescrevam digitações mais recentes, causa comum de mensagens de erro “aleatórias”.

Qual é a abordagem segura por padrão para UI otimista ao salvar um formulário?

Atualize a UI imediatamente para refletir a ação (por exemplo, mostrar Saving… e manter os inputs visíveis), mas mantenha sempre um caminho de rollback caso o servidor rejeite. Use um id/versão de requisição, desative ou debounced o botão Salvar e defina o que edições durante o salvamento significam (bloquear campos, enfileirar outro salvamento ou marcar como sujo novamente).

Como estruturar estados de erro para que usuários possam recuperar sem reescrever?

Nunca apague a entrada do usuário em caso de falha. Coloque problemas específicos de campo inline no campo relevante, mantenha erros ao nível do formulário perto da ação de envio e torne falhas de rede recuperáveis com uma tentativa que reenvie o mesmo payload, a menos que o usuário altere algo.

Onde devem ficar eventos pontuais como snackbars e navegação?

Mantenha efeitos pontuais fora do seu estado persistente. Em MVVM, envie-os por um fluxo separado (como um SharedFlow); em MVI, modele-os como Effects que a UI consome uma vez. Isso evita snackbars duplicados ou navegação repetida após rotação ou re-subscrição.

Exatamente o que eu devo salvar para rascunhos offline de um formulário?

Persista principalmente a entrada bruta do usuário (como digitada), mais metadados mínimos para restaurar e mesclar com segurança depois, como um marcador de versão do servidor. Recompute a validação na restauração em vez de persistí-la e adicione uma versão simples de esquema para lidar com atualizações do app sem quebrar restaurações.

Como temporizar o autosave para que seja confiável mas não excessivo?

Use um debounce curto (alguns centenas de milissegundos) mais salvamentos em mudanças de etapa ou quando o usuário manda para background. Salvar a cada tecla é barulhento e pode gerar contenção, enquanto salvar apenas na saída corre o risco de perder trabalho em morte de processo ou interrupções.

Como tratar conflitos de rascunho quando os dados do servidor mudaram enquanto o usuário estava offline?

Mantenha um marcador de versão (como updatedAt, um ETag ou um incremento local) para o snapshot do servidor e para o rascunho. Se a versão do servidor não mudou, aplique o rascunho e envie; se mudou, mostre uma escolha clara para manter o rascunho ou recarregar os dados do servidor, em vez de sobrescrever silenciosamente qualquer lado.

Fácil de começar
Criar algo espantoso

Experimente o AppMaster com plano gratuito.
Quando estiver pronto, você poderá escolher a assinatura adequada.

Comece