MVC vs MVP vs MVVM vs MVI en Android: ¿cuál y por qué?
Todo patrón de arquitectura promete código de UI testeable y desacoplado — con esta pregunta los entrevistadores comprueban si entiendes los tradeoffs detrás de las siglas, no solo las siglas.
APRENDE HACIENDOPon a prueba tu intuición3 pasos
MVC vs MVP vs MVVM vs MVI en Android: ¿cuál y por qué?
Usa los ejemplos y la explicación de la lección para comprobar tu idea.
Reflexión personal. No se envía a ningún servicio de IA.#Por qué esta pregunta sigue apareciendo
En cualquier entrevista de Android que supere el nivel junior, tarde o temprano aparece alguna versión de "compara MVC, MVP, MVVM y MVI". Parece un examen de vocabulario, pero no lo es: es una forma indirecta de comprobar si entiendes la separación de responsabilidades, si sabes explicar la capacidad de testeo sin quedarte en la frase genérica "hago pruebas unitarias", y si conoces cómo interactúa cada patrón con el ciclo de vida de la Activity o el Fragment, que es justo donde estos patrones suelen romperse en aplicaciones reales. Una respuesta junior enumera cuatro siglas. Una respuesta senior explica qué problema resolvió cada patrón respecto al anterior, y es capaz de decir, para una pantalla concreta, cuál elegiría y por qué.
#MVC: el patrón del que Android tomó el nombre, no la forma
A Android se le suele llamar "MVC por defecto", pero nunca tuvo un MVC limpio en el sentido clásico de escritorio. La Activity o el Fragment terminan haciendo de View y de Controller a la vez: infla el layout, guarda referencias a los widgets y además contiene los manejadores de clics y la lógica de negocio. El Model es cualquier clase de datos o repositorio que definas tú mismo. No existe una frontera impuesta por el framework, y ese es justo el problema: la Activity se convierte en un "objeto todopoderoso" difícil de testear unitariamente, porque tocar cualquiera de sus lógicas implica tocar clases del framework de Android que solo funcionan correctamente en un dispositivo o en Robolectric.
#MVP: saca la lógica fuera, deja la View tonta
Model-View-Presenter resuelve el problema de testeo introduciendo un Presenter que concentra toda la lógica y habla con la View solo a través de una interfaz estrecha (interface UserView { fun showUser(user: User); fun showError(message: String) }). La Activity implementa esa interfaz y no hace nada más que pintar: sin decisiones, sin lógica. Como el Presenter no tiene dependencias de Android, puedes testearlo con JUnit puro y una View simulada (mock). La contrapartida es el boilerplate: cada pantalla necesita una interfaz de View, un Presenter y una gestión de ciclo de vida cuidadosa, porque el Presenter guarda una referencia a la View y hay que anularla en onDestroy o se produce una fuga de memoria de la Activity.
#MVVM: ninguna referencia de vuelta a la View
Model-View-ViewModel elimina incluso esa referencia. El ViewModel expone estado observable — históricamente LiveData, hoy más a menudo StateFlow — y la View (Activity, Fragment o Composable) se suscribe a él. El ViewModel nunca guarda un puntero a la View, así que no hay nada que se pueda fugar ni nada que anular. La clase ViewModel de Jetpack añade, además del patrón en sí, un beneficio muy práctico: sobrevive automáticamente a los cambios de configuración, porque el framework retiene el ViewModelStore a través de la recreación de la Activity. Por eso Google recomienda MVVM como opción por defecto para la mayoría de pantallas: obtienes el patrón de arquitectura y un componente consciente del ciclo de vida en una sola clase. La trampa clásica de MVVM son los eventos de un solo uso (navegación, snackbars, toasts): si simplemente vuelves a emitir el último valor a cada nuevo suscriptor, el evento se dispara de nuevo al rotar la pantalla, por lo que los equipos recurren a SharedFlow sin replay, a Channel, o a un tipo de evento envuelto que se marca como "consumido".
#MVI: un solo estado, una sola dirección, sin excepciones
Model-View-Intent lleva la idea de "la View no guarda su propio estado" hasta su conclusión lógica: toda la pantalla tiene exactamente un objeto State inmutable, y la única forma de cambiarlo es despachando un Intent (una acción del usuario, que no debe confundirse con la clase Intent de Android) que un reducer transforma en un nuevo State. Nada se muta in situ; cada actualización reemplaza el State completo. Esto hace que el sistema sea trivial de razonar y de testear — dado un State y un Intent, el siguiente State es una función pura — y encaja de forma muy natural con Compose, que en sí mismo es un modelo de renderizado unidireccional y guiado por estado. El costo es la ceremonia: incluso una pantalla con un solo booleano necesita una data class State, una jerarquía sellada de Intent y un reducer, lo que puede parecer excesivo en un formulario simple.
#Comparativa lado a lado
| Patrón | La View guarda | Testeable sin Android | Sobrevive cambios de config | Boilerplate típico |
|---|---|---|---|---|
| MVC | lógica + referencias | Malo | No | Bajo |
| MVP | solo la interfaz | Bueno | No (requiere cuidado) | Medio-alto |
| MVVM | suscripción a estado | Bueno | Sí (ViewModel) | Medio |
| MVI | suscripción a un único State | Muy bueno | Sí (ViewModel) | Alto |
#Un ejemplo mínimo de MVVM
data class ProfileUiState(val isLoading: Boolean = true,val userName: String? = null,val errorMessage: String? = null)class ProfileViewModel(private val repository: UserRepository) : ViewModel() {private val _uiState = MutableStateFlow(ProfileUiState())val uiState: StateFlow<ProfileUiState> = _uiState.asStateFlow()fun loadProfile(userId: String) {viewModelScope.launch {_uiState.update { it.copy(isLoading = true, errorMessage = null) }runCatching { repository.getUser(userId) }.onSuccess { user ->_uiState.update { it.copy(isLoading = false, userName = user.name) }}.onFailure { error ->_uiState.update { it.copy(isLoading = false, errorMessage = error.message) }}}}}@Composablefun ProfileScreen(viewModel: ProfileViewModel) {val state by viewModel.uiState.collectAsStateWithLifecycle()when {state.isLoading -> LoadingSpinner()state.errorMessage != null -> ErrorMessage(state.errorMessage)else -> Text(text = "Hello, ${state.userName}")}}
Fíjate en que el ViewModel nunca importa nada de android.view ni guarda una referencia a la Activity o al Fragment: solo conoce UserRepository y expone un StateFlow. Eso es lo que lo hace trivialmente testeable: sustituyes el repositorio por uno falso, llamas a loadProfile y verificas uiState.value.
🧠 Check your understanding
0/3 · 0/3 answered1. ¿Cuál es la principal ventaja de testeo que comparten MVP y MVVM frente a un MVC simple en Android?
2. ¿Por qué MVP requiere un manejo cuidadoso en onDestroy que MVVM no necesita?
3. ¿Qué distingue más claramente a MVI de MVVM?