31 may 2025·7 min de lectura

Kotlin MVI vs MVVM para apps Android con muchos formularios: estados de la UI

Kotlin MVI vs MVVM para apps Android con muchos formularios: explicado con formas prácticas de modelar validación, UI optimista, estados de error y borradores offline.

Kotlin MVI vs MVVM para apps Android con muchos formularios: estados de la UI

Por qué las apps Android con muchos formularios se complican rápido

Las apps con muchos formularios parecen lentas o frágiles porque los usuarios están constantemente esperando decisiones pequeñas que tu código debe tomar: ¿este campo es válido?, ¿la guardado funcionó?, ¿debemos mostrar un error?, y ¿qué pasa si la red cae?.

Los formularios también revelan primero los bugs de estado porque mezclan varios tipos de estado a la vez: estado de UI (lo visible), estado de entrada (lo que escribió el usuario), estado del servidor (lo guardado) y estado temporal (lo que está en progreso). Cuando eso se desincroniza, la app empieza a sentirse “aleatoria”: botones que se deshabilitan en el momento equivocado, errores antiguos que persisten o la pantalla que se reinicia tras una rotación.

La mayoría de problemas se agrupan en cuatro áreas: validación (especialmente reglas entre campos), UI optimista (feedback rápido mientras el trabajo corre), manejo de errores (fallos claros y recuperables) y borradores offline (no perder trabajo sin terminar).

Una buena UX de formularios sigue unas reglas simples:

  • La validación debe ser útil y cercana al campo. No bloquees la escritura. Sé estricto cuando importe, normalmente al enviar.
  • La UI optimista debe reflejar la acción del usuario inmediatamente, pero también necesita una reversión limpia si el servidor la rechaza.
  • Los errores deben ser específicos, accionables y nunca borrar la entrada del usuario.
  • Los borradores deben sobrevivir reinicios, interrupciones y conexiones malas.

Por eso los debates de arquitectura se ponen intensos para formularios. El patrón que elijas decide cuán predecibles se sienten esos estados bajo presión.

Repaso rápido: MVVM y MVI en términos llanos

La diferencia real entre MVVM y MVI es cómo fluye el cambio por la pantalla.

MVVM (Model View ViewModel) suele verse así: el ViewModel mantiene datos de pantalla, los expone a la UI (a menudo vía StateFlow o LiveData) y ofrece métodos como save, validate o load. La UI llama a funciones del ViewModel cuando el usuario interactúa.

MVI (Model View Intent) suele verse así: la UI envía eventos (intents), un reducer los procesa, y la pantalla se renderiza a partir de un objeto de estado que representa todo lo que la UI necesita ahora mismo. Los efectos secundarios (red, base de datos) se disparan de forma controlada y reportan resultados de vuelta como eventos.

Una forma simple de recordar la mentalidad:

  • MVVM pregunta: “¿Qué datos debe exponer el ViewModel y qué métodos debe ofrecer?”
  • MVI pregunta: “¿Qué eventos pueden ocurrir y cómo transforman un estado en otro?”

Cualquiera de los dos funciona bien para pantallas simples. Cuando añades validación entre campos, autoguardado, reintentos y borradores offline, necesitas reglas más estrictas sobre quién puede cambiar el estado y cuándo. MVI aplica esas reglas por defecto. MVVM puede seguir funcionando bien, pero requiere disciplina: rutas de actualización consistentes y manejo cuidadoso de eventos puntuales de UI (toasts, navegación).

Cómo modelar el estado del formulario sin sorpresas

La forma más rápida de perder el control es dejar que los datos del formulario vivan en demasiados sitios: enlaces de vista, múltiples flows y “una booleana más”. Las pantallas con muchos formularios se mantienen previsibles cuando hay una sola fuente de verdad.

Una forma práctica de FormState

Apunta a un único FormState que contenga entradas crudas más unas pocas banderas derivadas en las que puedas confiar. Manténlo aburrido y completo, incluso si parece un poco 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 }

Esto mantiene la validación a nivel de campo (por entrada) separada de problemas a nivel de formulario (como “el total debe ser \u003e 0”). Las banderas derivadas como isDirty e isValid deben calcularse en un solo lugar, no reimplementarse en la UI.

Un modelo mental limpio es: campos (lo que escribió el usuario), validación (qué está mal), estado (qué está haciendo la app), suciedad (qué cambió desde el último guardado) y borradores (si existe una copia offline).

Dónde van los efectos puntuales

Los formularios también disparan eventos únicos: snackbars, navegación, banners de “guardado”. No pongas estos dentro de FormState, o se volverán a disparar en una rotación o cuando la UI se re-suscriba.

En MVVM, emite efectos a través de un canal separado (por ejemplo, un SharedFlow). En MVI, módelos como Effects (o Events) que la UI consume una vez. Esta separación previene “errores fantasma” y mensajes de éxito duplicados.

Flujo de validación en MVVM vs MVI

La validación es donde las pantallas de formulario empiezan a sentirse frágiles. La decisión clave es dónde viven las reglas y cómo regresan los resultados a la UI.

Las reglas simples y síncronas (campos obligatorios, longitud mínima, rangos numéricos) deberían ejecutarse en el ViewModel o en la capa de dominio, no en la UI. Eso mantiene las reglas testeables y consistentes.

Las reglas asíncronas (como “¿este email ya está en uso?”) son más complicadas. Necesitas manejar carga, resultados obsoletos y el caso de “el usuario volvió a escribir”.

En MVVM, la validación a menudo se vuelve una mezcla de estado y métodos auxiliares: la UI envía cambios (actualizaciones de texto, pérdida de foco, clicks de submit) al ViewModel; el ViewModel actualiza un StateFlow/LiveData y expone errores por campo y un derivado “canSubmit”. Las comprobaciones asíncronas suelen iniciar un job, luego actualizan una bandera de carga y un error cuando terminan.

En MVI, la validación tiende a ser más explícita. Una división práctica de responsabilidades es:

  • El reducer ejecuta validación síncrona y actualiza errores por campo inmediatamente.
  • Un effect ejecuta validación asíncrona y despacha un intent con el resultado.
  • El reducer aplica ese resultado solo si aún coincide con la entrada más reciente.

Ese último paso importa. Si el usuario escribe un nuevo email mientras la comprobación de “email único” está en curso, los resultados antiguos no deberían sobrescribir la entrada actual. MVI suele facilitar codificar eso porque puedes guardar el último valor comprobado en el estado e ignorar respuestas obsoletas.

UI optimista y guardados asíncronos

Mantén informados a los usuarios automáticamente
Envía confirmaciones y alertas por email, SMS o Telegram desde la lógica de tu proceso.
Agregar mensajería

UI optimista significa que la pantalla se comporta como si el guardado hubiera funcionado antes de que llegue la respuesta de red. En un formulario, eso suele significar que el botón Guardar pasa a “Guardando…”, aparece un pequeño indicador de “Guardado” cuando termina, y los inputs siguen siendo editables (o se bloquean intencionalmente) mientras la petición está en vuelo.

En MVVM, esto se implementa comúnmente alternando flags como isSaving, lastSavedAt y saveError. El riesgo es la deriva: guardados solapados pueden dejar esas banderas inconsistentes. En MVI, un reducer actualiza un único objeto de estado, por lo que “Guardando” y “Deshabilitado” tienen menos probabilidad de contradecirse.

Para evitar envíos dobles y condiciones de carrera, trata cada guardado como un evento identificado. Si el usuario toca Guardar dos veces o edita durante un guardado, necesitas una regla sobre qué respuesta gana. Unas cuantas salvaguardas funcionan en ambos patrones: deshabilitar Guardar mientras se guarda (o debouncear taps), adjuntar un requestId (o versión) a cada guardado e ignorar respuestas obsoletas, cancelar trabajo en vuelo cuando el usuario sale, y definir qué significan las ediciones durante el guardado (enviar otro guardado en cola o marcar el formulario como sucio de nuevo).

El éxito parcial también es común: el servidor acepta algunos campos pero rechaza otros. Módelalo explícitamente. Mantén errores por campo (y, si hace falta, estado de sincronización por campo) para poder mostrar “Guardado” en general mientras aún resaltas un campo que necesita atención.

Estados de error de los que los usuarios pueden recuperarse

Soporta formularios con herramientas administrativas
Crea herramientas internas para revisar envíos, corregir errores y soportar reintentos.
Crear panel de administración

Las pantallas de formulario fallan de más maneras que “algo salió mal”. Si cada fallo se convierte en un toast genérico, los usuarios reescriben datos, pierden confianza y abandonan el flujo. La meta siempre es la misma: mantener la entrada segura, mostrar una solución clara y hacer que reintentar sea natural.

Ayuda separar los errores según dónde pertenecen. Un formato de email incorrecto no es lo mismo que una caída de servidor.

Los errores de campo deben ser inline y ligados a la entrada correspondiente. Los errores a nivel de formulario deben estar cerca de la acción de submit y explicar qué bloquea el envío. Los errores de red deben ofrecer reintento y mantener el formulario editable. Los errores de permisos o autenticación deben guiar al usuario a re-autenticarse preservando un borrador.

Una regla fundamental de recuperación: nunca borres la entrada del usuario al fallar. Si el guardado falla, conserva los valores actuales en memoria y en disco. Reintentar debe reenviar la misma carga a menos que el usuario la edite.

Donde los patrones difieren es cómo los errores del servidor se mapean de vuelta al estado de UI. En MVVM, es fácil actualizar múltiples flows o campos y crear inconsistencias accidentalmente. En MVI, normalmente aplicas la respuesta del servidor en un solo paso del reducer que actualiza fieldErrors y formError juntos.

También decide qué es estado y qué es efecto puntual. Los errores inline y “envío fallido” pertenecen al estado (deben sobrevivir rotaciones). Acciones puntuales como un snackbar, vibración o navegación deben ser efectos.

Borradores offline y restaurar formularios en progreso

Una app con muchos formularios se siente “offline” incluso cuando la red está bien. Los usuarios cambian de app, el SO mata tu proceso o pierden señal en mitad del flujo. Los borradores evitan que tengan que empezar de nuevo.

Primero, define qué significa un borrador. Guardar solo el modelo “limpio” suele no ser suficiente. Normalmente quieres restaurar la pantalla exactamente como estaba, incluyendo campos a medio escribir.

Lo que vale la pena persistir son mayormente entradas crudas del usuario (strings tal como se escribieron, IDs seleccionados, URIs de adjuntos), más metadatos suficientes para fusionar de forma segura después: una instantánea del servidor conocida y un marcador de versión (updatedAt, ETag o un simple incremento). La validación puede recomputarse al restaurar.

La elección de almacenamiento depende de sensibilidad y tamaño. Borradores pequeños pueden vivir en preferencias, pero formularios multi-paso y adjuntos son más seguros en una base de datos local. Si el borrador contiene datos personales, usa almacenamiento cifrado.

La pregunta de arquitectura más grande es dónde vive la fuente de verdad. En MVVM, los equipos a menudo persisten desde el ViewModel cada vez que cambian los campos. En MVI, persistir después de cada actualización del reducer puede ser más simple porque guardas un estado coherente (o un objeto Draft derivado).

El momento del autoguardado importa. Guardar en cada pulsación es ruidoso; un debounce corto (por ejemplo, 300 a 800 ms) más un guardado en cambio de paso funciona bien.

Cuando el usuario vuelve a estar online, necesitas reglas de fusión. Un enfoque práctico es: si la versión del servidor no cambió, aplica el borrador y envía. Si cambió, muestra una elección clara: conservar mi borrador o recargar los datos del servidor.

Paso a paso: implementar un formulario fiable con cualquiera de los dos patrones

Sal en vivo a tu manera
Despliega en AppMaster Cloud o en tu propio AWS, Azure, o Google Cloud.
Desplegar ahora

Los formularios fiables empiezan con reglas claras, no con código de UI. Cada acción de usuario debe llevar a un estado predecible, y cada resultado asíncrono debe tener un lugar obvio donde aterrizar.

Escribe las acciones a las que tu pantalla debe reaccionar: escribir, perder foco, enviar, reintentar y navegación entre pasos. En MVVM se convierten en métodos del ViewModel y actualizaciones de estado. En MVI son intents explícitos.

Luego construye en pequeñas pasadas:

  1. Define eventos para todo el ciclo de vida: edit, blur, submit, save success/failure, retry, restore draft.
  2. Diseña un objeto de estado: valores de campos, errores por campo, estado general del formulario y “tiene cambios no guardados”.
  3. Añade validación: comprobaciones ligeras durante la edición, comprobaciones más fuertes al enviar.
  4. Añade reglas de guardado optimista: qué cambia inmediatamente y qué dispara una reversión.
  5. Añade borradores: autoguardado con debounce, restauración al abrir y muestra un pequeño indicador de “borrador restaurado” para que los usuarios confíen en lo que ven.

Trata los errores como parte de la experiencia. Mantén la entrada, resalta solo lo que necesita corrección y ofrece una siguiente acción clara (editar, reintentar o conservar borrador).

Si quieres prototipar estados complejos de formularios antes de escribir UI Android, una plataforma sin código como AppMaster puede ser útil para validar el flujo primero. Luego puedes implementar las mismas reglas en MVVM o MVI con menos sorpresas.

Escenario de ejemplo: formulario multi-paso de reporte de gastos

Imagina un reporte de gastos de 4 pasos: detalles (fecha, categoría, importe), subida de recibo, notas y luego revisar y enviar. Tras enviar, muestra un estado de aprobación como Draft, Submitted, Rejected, Approved. Las partes complicadas son la validación, guardados que pueden fallar y mantener un borrador cuando el teléfono está offline.

En MVVM, típicamente mantienes un FormUiState en el ViewModel (a menudo un StateFlow). Cada cambio de campo llama a una función del ViewModel como onAmountChanged() o onReceiptSelected(). La validación corre al cambiar, al navegar de paso o al enviar. Una estructura común es entradas crudas más errores por campo, con flags derivados que controlan si Next/Submit están habilitados.

En MVI, el mismo flujo se vuelve explícito: la UI envía intents como AmountChanged, NextClicked, SubmitClicked y RetrySave. Un reducer devuelve un nuevo estado. Los efectos secundarios (subir recibo, llamar al API, mostrar un snackbar) corren fuera del reducer y devuelven resultados como eventos.

En la práctica, MVVM facilita añadir funciones y actualizar un flow rápidamente. MVI hace más difícil saltarse una transición de estado por accidente porque cada cambio se canaliza por el reducer.

Errores comunes y trampas

Prototipa formularios complejos más rápido
Modela tu flujo de formularios una vez y genera backend, web y apps móviles desde él.
Probar AppMaster

La mayoría de bugs de formularios vienen de reglas poco claras sobre quién posee la verdad, cuándo se ejecuta la validación y qué pasa cuando llegan resultados asíncronos tarde.

El error más común es mezclar fuentes de verdad. Si un campo de texto a veces lee de un widget, a veces del estado del ViewModel y a veces de un borrador restaurado, tendrás reinicios aleatorios y reportes de “mi entrada desapareció”. Elige un estado canónico para la pantalla y deriva todo lo demás de él (modelo de dominio, filas de caché, payloads API).

Otra trampa fácil es confundir estado con eventos. Un toast, navegación o banner de “¡Guardado!” es algo puntual. Un mensaje de error que debe quedarse visible hasta que el usuario edite es estado. Mezclarlos causa efectos duplicados en rotación o retroalimentación perdida.

Dos problemas de corrección aparecen a menudo:

  • Sobre-validar en cada pulsación, especialmente para comprobaciones costosas. Usa debounce, valida al perder foco o solo campos tocados.
  • Ignorar resultados asíncronos fuera de orden. Si el usuario guarda dos veces o edita después de guardar, respuestas antiguas pueden sobrescribir entradas nuevas a menos que uses ids de petición (o lógica de “solo el último”).

Finalmente, los borradores no son “solo guardar JSON”. Sin versionado, las actualizaciones de la app pueden romper restauraciones. Añade una versión de esquema simple y una historia de migración, aunque sea “borrar y empezar de nuevo” para borradores muy antiguos.

Lista rápida antes de enviar

Haz predecibles los cambios de estado
Convierte tus reglas de validación y guardado en Business Processes que puedas probar temprano.
Comenzar a crear

Antes de discutir MVVM vs MVI, asegúrate de que tu formulario tenga una única fuente de verdad. Si un valor puede cambiar en pantalla, pertenece al estado, no a un widget de vista o a una bandera oculta.

Una comprobación práctica antes de enviar:

  • El estado incluye entradas, errores por campo, estado de guardado (idle/saving/saved/failed) y estado de borrador/cola para que la UI nunca tenga que adivinar.
  • Las reglas de validación son puras y testeables sin UI.
  • La UI optimista tiene una vía de reversión para rechazos del servidor.
  • Los errores nunca borran la entrada del usuario.
  • La restauración de borradores es predecible: o un banner claro de auto-restauración o una acción explícita “Restaurar borrador”.

Una prueba más que detecta bugs reales: activa modo avión a mitad de un guardado, apágalo y reintenta dos veces. El segundo reintento no debería crear un duplicado. Usa un request ID, clave de idempotencia o un marcador local de “guardado pendiente” para que los reintentos sean seguros.

Si tus respuestas son difusas, ajusta primero el modelo de estado, luego elige el patrón que haga esas reglas más fáciles de aplicar.

Próximos pasos: elegir un camino y construir más rápido

Empieza con una pregunta: ¿qué tan costoso es que tu formulario quede en un estado medio actualizado? Si el costo es bajo, mantenlo simple.

MVVM encaja bien cuando la pantalla es directa, el estado es mayormente “campos + errores” y tu equipo ya entrega con confianza usando ViewModel + LiveData/StateFlow.

MVI encaja mejor cuando necesitas transiciones de estado estrictas y predecibles, muchos eventos asíncronos (autoguardado, reintentos, sincronización) o cuando los bugs son caros (pagos, cumplimiento, flujos críticos).

Sea cual sea el camino, las pruebas de mayor retorno para formularios suelen no tocar la UI: casos límites de validación, transiciones de estado (editar, enviar, éxito, fallo, reintento), reversión de guardado optimista y restauración de borradores más comportamiento de conflictos.

Si también necesitas backend, pantallas administrativas y APIs junto a tu app móvil, AppMaster (appmaster.io) puede generar backend listo para producción, web y apps nativas desde un único modelo, lo que ayuda a mantener las reglas de validación y flujo consistentes en todas las superficies.

FAQ

When should I choose MVVM vs MVI for a form-heavy Android screen?

Elige MVVM cuando el flujo del formulario sea mayormente lineal y tu equipo ya tenga convenciones sólidas para StateFlow/LiveData, eventos puntuales y cancelaciones. Elige MVI cuando esperes mucho trabajo asíncrono superpuesto (autoguardado, reintentos, subidas) y necesites reglas más estrictas para evitar que cambios de estado se "cuelen" desde múltiples sitios.

What’s the simplest way to keep form state from drifting out of sync?

Empieza con un único objeto de estado de pantalla (por ejemplo, FormState) que contenga valores crudos de campos, errores por campo, un error a nivel de formulario y estados claros como Saving o Failed. Mantén flags derivados como isValid y canSubmit calculados en un solo lugar para que la UI solo renderice y no vuelva a decidir la lógica.

How often should validation run in a form: on every keystroke or only on submit?

Haz comprobaciones ligeras y baratas mientras el usuario escribe (requerido, rango, formato básico), y ejecútalas estrictas al enviar. Mantén la lógica de validación fuera de la UI para que sea testeable y guarda los errores en el estado para que sobrevivan rotaciones y restauraciones tras muerte de proceso.

How do I handle async validation like “email already taken” without stale results?

Trata la validación asíncrona como “gana la entrada más reciente”. Guarda el valor que validaste (o un id/version de la petición) e ignora resultados que no coincidan con el estado actual. Así evitas que respuestas obsoletas sobrescriban una escritura nueva, que es una causa común de mensajes de error “aleatorios”.

What’s a safe default approach for optimistic UI when saving a form?

Actualiza la UI inmediatamente para reflejar la acción (por ejemplo, muestra Saving… y mantiene el input visible), pero siempre ten una vía de reversión si el servidor rechaza el guardado. Usa un id/version de petición, deshabilita o debouncea el botón Guardar, y define qué significan las ediciones durante el guardado (bloquear campos, encolar otro guardado o marcar como sucio de nuevo).

How should I structure error states so users can recover without retyping?

Nunca borres la entrada del usuario tras un fallo. Muestra problemas específicos en el campo correspondiente, coloca errores a nivel de formulario cerca del botón de envío y haz que fallos de red sean recuperables con un reintento que reenvíe la misma carga salvo que el usuario la modifique.

Where should one-time events like snackbars and navigation live?

Saca los efectos puntuales del estado persistente. En MVVM, emítelos por un stream separado (como un SharedFlow), y en MVI, módelos como Effects que la UI consuma una vez. Así evitas snackbars duplicados o navegaciones repetidas tras rotaciones o re-suscripciones.

What exactly should I save for offline drafts of a form?

Persiste principalmente la entrada cruda del usuario (tal como se escribió), más metadatos mínimos para restaurar y fusionar de forma segura más tarde, como un marcador de versión del servidor. Recalcula la validación al restaurar en lugar de persistirla, y añade una versión de esquema simple para poder manejar actualizaciones de la app sin romper restauraciones.

How should autosave be timed so it feels reliable but not noisy?

Usa un debounce corto (alrededor de unos cientos de milisegundos) más guardados en cambios de paso o cuando el usuario envía la app al fondo. Guardar en cada pulsación es ruidoso y puede generar contención extra; guardar solo al salir puede perder trabajo en una muerte de proceso o interrupciones.

How do I handle draft conflicts when the server data changed while the user was offline?

Mantén un marcador de versión (como updatedAt, un ETag o un incremento local) tanto para la instantánea del servidor como para el borrador. Si la versión del servidor no cambió, aplica el borrador y envía; si cambió, muestra una elección clara: conservar mi borrador o recargar los datos del servidor, en lugar de sobrescribir silenciosamente cualquiera de los dos.

Fácil de empezar
Crea algo sorprendente

Experimente con AppMaster con plan gratuito.
Cuando esté listo, puede elegir la suscripción adecuada.

Empieza