La genealogía: de MVC a MVP, a MVVM y a MVI
Los patrones de presentación de Android no son estilos intercambiables sino respuestas sucesivas a un mismo problema que nunca terminó de resolverse: dónde vive la lógica de una pantalla y quién tiene permiso para escribir su estado. Esta lección reconstruye la genealogía completa, desde la Activity que lo hacía todo hasta el estado único de MVI, explicando qué dolor concreto motivó cada salto, qué compró y qué factura dejó pendiente para el siguiente. Entender la cadena importa más que memorizar las siglas, porque cada patrón solo tiene sentido como corrección del anterior y quien ignora el problema original acaba reintroduciéndolo bajo otro nombre.
Nadie inventó MVI una mañana por gusto estético. Cada patrón de presentación que ha pasado por Android nació como respuesta a un dolor concreto y verificable, y murió —o mejor dicho, cedió terreno— cuando su propia solución generó un dolor nuevo. La cadena MVC, MVP, MVVM, MVI no es una escalera de calidad donde el último escalón es siempre el mejor: es una secuencia de negociaciones sobre la misma pregunta que la plataforma nunca contestó bien de fábrica. Quién es dueño del estado de la pantalla, quién puede escribirlo y cómo se entera la interfaz de que cambió. Leer esta genealogía sin las siglas por delante, atendiendo solo al problema que cada salto intentaba cerrar, es lo que convierte una discusión de arquitectura en una decisión informada. Y es también la única vacuna contra el error más común del oficio: adoptar el patrón de moda sin haber sufrido nunca el problema que ese patrón existe para resolver.
- Reconstruir la cadena
MVC,MVP,MVVM,MVIcomo respuestas sucesivas a un mismo problema de propiedad del estado. - Identificar el dolor concreto de Android que motivó cada salto y la factura que dejó pendiente.
- Distinguir lo que cada patrón compró de verdad de lo que solo prometió en los artículos de su época.
- Reconocer cuándo un equipo adopta un patrón sin haber padecido el problema que lo justifica.
La Activity que lo era todo
El punto de partida no fue MVC en su sentido académico, sino algo peor: la Activity como clase única donde convivían la búsqueda de vistas por identificador, las llamadas de red, el acceso a base de datos, el formateo de fechas y la lógica de negocio. La plataforma empujaba a eso porque el ciclo de vida obligaba a que todo colgase de la Activity, y porque los primeros ejemplos oficiales enseñaban exactamente ese estilo.
class PerfilActivity : Activity() {
private var nombre: String = ""
private var cargando: Boolean = false
override fun onCreate(estado: Bundle?) {
super.onCreate(estado)
setContentView(R.layout.perfil)
cargando = true
Thread {
val perfil = ApiCliente.pedirPerfil() // red en la misma clase
runOnUiThread {
nombre = perfil.nombre // estado en un campo de la Activity
cargando = false
findViewById<TextView>(R.id.nombre).text = nombre
}
}.start()
}
}
El dolor fue inmediato y de dos clases. El primero, imposibilidad de probar: cualquier comprobación exigía un emulador, porque la lógica estaba entretejida con clases del framework que no existen en la máquina virtual de escritorio. El segundo, la rotación. Al girar el dispositivo, la Activity moría y renacía, y con ella todo el estado que vivía en sus campos. El estado no tenía dueño: vivía de prestado en un objeto que el sistema podía destruir en cualquier momento.
Conviene ser preciso con la historia: lo que la comunidad llamaba MVC en Android rara vez lo era. En el MVC clásico el controlador recibe la entrada y el modelo notifica a la vista. En Android la Activity era vista y controlador a la vez, y el modelo eran unas cuantas variables sueltas. La primera arquitectura de la plataforma no fue un patrón mal elegido, sino la falta de cualquier frontera entre capas. Todo lo que vino después es, en el fondo, un intento de dibujar esa frontera en algún sitio.
MVP: sacar la lógica del framework
MVP fue la primera respuesta seria y su objetivo era estrictamente uno: hacer la lógica comprobable sin emulador. La receta consistía en definir una interfaz de vista con métodos pasivos, y un presentador que la manipulaba sin conocer nada de Android.
interface PerfilView {
fun mostrarCargando()
fun mostrarNombre(nombre: String)
fun mostrarError(mensaje: String)
}
class PerfilPresenter(private val view: PerfilView, private val repo: Repo) {
fun cargar() {
view.mostrarCargando()
repo.pedirPerfil { resultado ->
if (resultado.ok) view.mostrarNombre(resultado.nombre)
else view.mostrarError(resultado.mensaje)
}
}
}
La ganancia fue real: el presentador se probaba con un doble de la interfaz, sin emulador, en milisegundos. Pero la factura llegó pronto y tenía tres partidas. La primera, la explosión de interfaces: cada pantalla exigía un contrato con diez o quince métodos que había que declarar, implementar y mantener sincronizados. La segunda, la fuga de memoria: el presentador guardaba una referencia a la vista y había que acordarse de soltarla en el momento exacto del ciclo de vida, o la Activity destruida quedaba retenida. La tercera, y la más grave, el estado implícito.
Porque MVP nunca resolvió de quién es el estado. El presentador no guardaba una fotografía de la pantalla; emitía órdenes imperativas. El estado real de la interfaz era la suma de todas las órdenes recibidas hasta ese instante, un dato que no existía en ninguna variable y que nadie podía inspeccionar. Tras una rotación, el presentador no sabía qué había mostrado ya, así que había que reconstruirlo a mano llamando de nuevo a los métodos correctos en el orden correcto.
La consecuencia práctica de ese estado fantasma se veía en los tests, que comprobaban órdenes y no resultados. Un test típico de MVP afirmaba que se había invocado mostrarCargando antes que mostrarNombre, es decir, verificaba la conversación entre dos objetos en lugar de verificar cómo quedaba la pantalla. Cuando alguien reordenaba dos líneas del presentador sin cambiar nada visible, el test fallaba; cuando alguien olvidaba apagar el indicador de carga, el test seguía pasando. Es el síntoma clásico de una comprobación acoplada al mecanismo y no al efecto.
MVVM: observables y el fin de las órdenes
MVVM llegó con dos armas nuevas que la plataforma por fin ofrecía de fábrica: el ViewModel de Jetpack, que sobrevive a los cambios de configuración, y los observables, LiveData primero y StateFlow después. El salto conceptual fue pasar de órdenes a datos observables: en lugar de decirle a la vista qué hacer, el ViewModel publica valores y la vista se suscribe.
class PerfilViewModel(private val repo: Repo) : ViewModel() {
val nombre = MutableStateFlow("")
val cargando = MutableStateFlow(false)
val error = MutableStateFlow<String?>(null)
fun cargar() {
cargando.value = true
viewModelScope.launch {
val r = repo.pedirPerfil()
cargando.value = false
if (r.ok) nombre.value = r.nombre else error.value = r.mensaje
}
}
}
Tres problemas de MVP desaparecieron de golpe. La interfaz de vista con quince métodos se evaporó, porque la vista ya no recibe órdenes sino que lee valores. La fuga de memoria se evaporó, porque el ViewModel no conoce a la vista y no puede retenerla. Y la rotación dejó de doler, porque el ViewModel sobrevive y sus observables reemiten el último valor a la vista recién nacida.
flowchart TB A[Activity que lo hace todo] -->|no se puede probar| B[MVP presentador con ordenes] B -->|interfaces enormes y fugas| C[MVVM observables y ViewModel] C -->|estado disperso y mutable| D[MVI estado unico y entrada unica] style A fill:#f38ba8,color:#11111b style C fill:#f9e2af,color:#11111b style D fill:#a6e3a1,color:#11111b
Pero MVVM dejó viva la pregunta original, solo que en forma más suave. El estado ya tenía dueño —el ViewModel— pero seguía disperso en varios observables independientes, cada uno mutable y cada uno capaz de cambiar en un instante distinto. La pantalla volvía a ser la suma de piezas que nadie garantizaba coherentes entre sí.
Hubo además una rama de MVVM que agravó el problema en lugar de aliviarlo: el enlace de datos de dos vías, que permitía a la vista escribir directamente en el observable del ViewModel. Era cómodo para formularios y devolvía el estado a un régimen de propiedad compartida, con dos escritores y ninguna autoridad que arbitrase. La llegada de Compose retiró esa rama de la circulación casi por completo, porque una interfaz declarativa que se redibuja en función de un valor no tiene dónde encajar una escritura de vuelta implícita.
Merece la pena señalar que el paso a Compose empujó a toda la comunidad en la dirección de MVI sin que nadie lo llamase así. Una interfaz declarativa se define como una función del estado, y una función necesita un argumento: cuanto más disperso está ese argumento, más incómodo resulta escribir la función. Compose no obliga a usar MVI, pero hace que el estado unificado sea el camino de menor resistencia, y esa presión explica buena parte de la adopción del patrón entre 2021 y hoy.
MVI: cerrar el círculo
MVI no aporta un contenedor nuevo ni un mecanismo nuevo: aporta dos prohibiciones sobre el ViewModel que ya existía. Una sola salida, un estado inmutable que se reemplaza entero. Una sola entrada, un canal por el que la interfaz envía datos en lugar de llamar a métodos variados.
data class PerfilState(val nombre: String = "", val contenido: Contenido = Contenido.Cargando)
sealed interface PerfilIntent {
data object Cargar : PerfilIntent
data class NombreCambiado(val texto: String) : PerfilIntent
}
class PerfilViewModel : ViewModel() {
private val _state = MutableStateFlow(PerfilState())
val state: StateFlow<PerfilState> = _state.asStateFlow()
fun onIntent(intent: PerfilIntent) = _state.update { reduce(it, intent) }
}
Su deuda genealógica es visible en cada pieza. El contenedor que sobrevive a la rotación lo hereda de MVVM. La idea de que la lógica debe vivir fuera del framework y ser comprobable sin emulador la hereda de MVP. Lo único propio es la disciplina: reunir lo que estaba disperso y cerrar lo que estaba abierto. Por eso resulta engañoso presentarlo como una alternativa a los anteriores; es más exacto verlo como el eslabón que recoge lo que cada uno resolvió y salda la factura que cada uno dejó.
MVP compró comprobabilidad
Sacó la lógica del framework y la hizo verificable sin emulador, al precio de interfaces enormes, fugas de memoria y un estado que solo existía como historia de órdenes.
MVVM compró supervivencia
El ViewModel resolvió la rotación y los observables eliminaron el contrato imperativo, pero repartieron el estado en piezas mutables e independientes.
MVI compró consistencia
Reunió esas piezas en un valor único e inmutable y cerró la entrada a un solo canal, al precio de una ceremonia que no toda pantalla amortiza.
Visto así, la cadena entera tiene una dirección clara: cada salto reduce el número de sitios desde los que se puede modificar la pantalla. En la Activity monolítica ese número era ilimitado. En MVP eran los métodos del presentador. En MVVM, los observables expuestos. En MVI, exactamente uno.
Si se quitan las siglas, los diagramas y las modas, lo que queda de treinta años de patrones de presentación es una única pregunta repetida con distinta ropa: cuántos sitios del programa tienen permiso para modificar lo que el usuario ve. Y la historia entera es un descenso monótono de ese número. La Activity monolítica permitía infinitos, porque cualquier línea de cualquier método podía tocar cualquier vista. MVP lo redujo a los métodos de un presentador, y a cambio inventó un estado fantasma que no vivía en ninguna variable. MVVM lo redujo a un puñado de observables con dueño claro, y a cambio permitió que esos observables se contradijeran entre sí. MVI lo reduce a uno. Nótese lo que esto implica: ningún patrón añadió capacidades: todos quitaron permisos. No hay un solo salto de esta cadena que permita expresar algo que antes no se podía expresar; todos los saltos prohíben caminos que antes estaban abiertos. Esa es la forma real en que progresa la arquitectura de software, y explica por qué las discusiones de patrones son tan estériles cuando se plantean como comparación de funcionalidades: no compiten en lo que dejan hacer, compiten en lo que impiden. De ahí se sigue el criterio que de verdad importa y que ninguna lista de ventajas te va a dar: un patrón solo te sirve si has padecido el problema que prohíbe. Adoptar MVI en una pantalla con dos campos es soldar puertas que nunca se abrieron; adoptar la Activity monolítica en un flujo de pago es dejar abiertas puertas que van a costar dinero. El programador maduro no pregunta cuál es el mejor patrón, sino cuántos escritores del estado puede tolerar esta pantalla sin volverse impredecible, y elige el eslabón de la cadena que corresponde a esa respuesta. Y sabe algo más, que es lo que separa la lectura histórica de la anécdota: cada patrón que se adopta sin entender su problema original acaba reintroduciendo ese problema bajo un nombre nuevo, porque el equipo replica la ceremonia sin replicar la disciplina que la ceremonia protegía.
- Busca en tu base de código —o en cualquier repositorio público con varios años de vida— una pantalla escrita en
MVPy anota cuántos métodos tiene su interfaz de vista. - Reescribe mentalmente esa pantalla en
MVVMy cuenta cuántos observables necesitarías; después cuenta cuántas combinaciones de valores permiten sus tipos. - Reduce esos observables a un solo estado inmutable y comprueba cuántas de esas combinaciones dejan de ser representables.
- Para cada salto de la cadena, escribe en una frase el dolor que resolvía y en otra la factura que dejaba pendiente.
- Elige una pantalla real de tu app y argumenta en qué eslabón de la cadena debería estar, usando el número de escritores del estado como criterio y no la moda vigente.