Patrones de diseño en Android: MVC, MVP, MVVM y MVI. Diferencias y cuándo usarlos
En Android, elegir una arquitectura no consiste en seguir una moda. El patrón que adoptes condiciona cómo organizas la lógica de presentación, cómo gestionas el estado de la pantalla y cuánto costará mantener la aplicación cuando crezca.
MVC, MVP, MVVM y MVI comparten un objetivo: evitar que Activity, Fragment o un composable concentren toda la responsabilidad de una pantalla. Sin embargo, cada patrón distribuye el trabajo de forma diferente y encaja mejor en ciertos escenarios.
El problema: pantallas con demasiadas responsabilidades
Una pantalla Android suele recibir eventos del usuario, pedir datos a un repositorio, transformar esos datos, mostrar estados de carga y gestionar errores. Cuando todo ocurre en una Activity o Fragment, el código se vuelve difícil de probar y modificar.
Una arquitectura saludable busca separar al menos estas piezas:
- Vista: muestra información y recoge interacciones.
- Lógica de presentación: decide qué debe mostrar la vista.
- Datos o dominio: obtiene, transforma y persiste información.
- Estado: representa la situación actual de la pantalla.
MVC: el enfoque clásico
MVC significa Model-View-Controller. Divide la aplicación en tres partes:
- Model: datos y reglas de negocio.
- View: interfaz que el usuario ve.
- Controller: recibe acciones de la vista y coordina cambios en el modelo.
En Android tradicional, una Activity o Fragment suele terminar haciendo de controlador. El problema es que también contiene referencias a vistas, lógica de navegación, llamadas a datos y manejo del ciclo de vida. Esto puede producir clases muy grandes.
class ProfileActivity : AppCompatActivity() {
private val repository = UserRepository()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
val user = repository.getUser()
findViewById<TextView>(R.id.nameText).text = user.name
}
}
}
Este ejemplo funciona, pero la pantalla depende directamente del repositorio y actualiza la interfaz por sí misma.
Cuándo usar MVC
MVC puede funcionar bien en:
- Aplicaciones pequeñas.
- Prototipos rápidos.
- Pantallas con lógica mínima.
- Proyectos antiguos donde cambiar toda la arquitectura no es viable.
Su principal riesgo aparece cuando el controlador crece demasiado. En Android, esto suele derivar en las conocidas Massive Activities o Massive Fragments.
MVP: una vista más pasiva
MVP significa Model-View-Presenter. La diferencia clave respecto a MVC es que el Presenter contiene la lógica de presentación y se comunica con una interfaz de vista.
La vista expone métodos como showLoading() o showUser(). El presenter decide cuándo invocarlos.
interface ProfileView {
fun showLoading()
fun showUser(name: String)
fun showError(message: String)
}
class ProfilePresenter(
private val view: ProfileView,
private val repository: UserRepository
) {
suspend fun loadProfile() {
view.showLoading()
runCatching { repository.getUser() }
.onSuccess { user -> view.showUser(user.name) }
.onFailure { error -> view.showError(error.message ?: "Error inesperado") }
}
}
La Activity o el Fragment implementan ProfileView, mientras que el presenter puede probarse sin depender directamente de componentes Android.
Ventajas de MVP
- Separa mejor la lógica de la interfaz.
- Facilita pruebas unitarias del presenter.
- Hace explícita la comunicación entre vista y presentación.
Limitaciones de MVP
El presenter suele mantener una referencia a la vista. Esto obliga a gestionar con cuidado el ciclo de vida para evitar llamadas cuando la pantalla ya no existe. Además, interfaces con muchos estados pueden acabar con decenas de métodos como showEmptyState(), hideLoading() o showRetryButton().
Cuándo usar MVP
MVP sigue siendo útil en proyectos basados en vistas XML, especialmente cuando ya existe una base de código madura con presenters. También puede encajar en equipos que prefieren interfaces explícitas entre la vista y la lógica.
MVVM: estado observable y ViewModel
MVVM significa Model-View-ViewModel. Es uno de los patrones más usados en Android moderno porque encaja de forma natural con ViewModel, StateFlow, LiveData y Jetpack Compose.
La vista observa un estado expuesto por el ViewModel. En lugar de recibir órdenes como showLoading(), la interfaz se renderiza según los datos disponibles.
data class ProfileUiState(
val isLoading: Boolean = false,
val name: String? = null,
val error: String? = null
)
class ProfileViewModel(
private val repository: UserRepository
) : ViewModel() {
private val _uiState = MutableStateFlow(ProfileUiState())
val uiState: StateFlow<ProfileUiState> = _uiState
fun loadProfile() {
viewModelScope.launch {
_uiState.value = ProfileUiState(isLoading = true)
_uiState.value = runCatching { repository.getUser() }
.fold(
onSuccess = { user -> ProfileUiState(name = user.name) },
onFailure = { error ->
ProfileUiState(error = error.message ?: "Error inesperado")
}
)
}
}
}
En Compose, la interfaz puede observar este estado y dibujarse de forma declarativa.
@Composable
fun ProfileScreen(viewModel: ProfileViewModel) {
val state by viewModel.uiState.collectAsState()
when {
state.isLoading -> CircularProgressIndicator()
state.error != null -> Text(state.error)
state.name != null -> Text("Hola, ${state.name}")
}
}
Cuándo usar MVVM
MVVM es una opción sólida para la mayoría de aplicaciones Android actuales, especialmente cuando:
- Usas Jetpack Compose.
- Necesitas sobrevivir a cambios de configuración.
- Trabajas con flujos de datos reactivos.
- Quieres separar interfaz y lógica sin crear interfaces de vista extensas.
- La pantalla tiene varios estados de carga, éxito o error.
MVI: estado único y eventos explícitos
MVI significa Model-View-Intent. Su idea central es que la pantalla tiene una única fuente de verdad: un estado completo e inmutable. La vista envía intenciones, el sistema las procesa y genera un nuevo estado.
El flujo suele ser unidireccional:
Usuario → Intent → ViewModel o Reducer → Nuevo estado → Vista
data class LoginUiState(
val email: String = "",
val password: String = "",
val isLoading: Boolean = false,
val error: String? = null
)
sealed interface LoginIntent {
data class EmailChanged(val value: String) : LoginIntent
data class PasswordChanged(val value: String) : LoginIntent
data object Submit : LoginIntent
}
Cada acción produce una transición de estado predecible. Esto facilita entender qué ha ocurrido en una pantalla compleja y reduce inconsistencias entre elementos visuales.
Cuándo usar MVI
MVI destaca cuando la interfaz tiene muchas interacciones o estados combinados:
- Formularios complejos.
- Flujos de pago o registro.
- Búsquedas con filtros.
- Pantallas de comercio electrónico.
- Interfaces con Compose y renderizado declarativo.
- Casos donde necesitas trazabilidad clara de eventos.
Su coste es una mayor cantidad de código: intents, reducers, efectos y estados. Para una pantalla simple, puede ser más estructura de la necesaria.
Comparativa rápida
| Patrón | Comunicación principal | Punto fuerte | Riesgo habitual |
|---|---|---|---|
| MVC | Vista y controlador | Simplicidad inicial | Actividades o fragments demasiado grandes |
| MVP | Vista mediante interfaz y presenter | Testabilidad explícita | Muchas llamadas y métodos de vista |
| MVVM | Estado observable desde ViewModel | Equilibrio entre simplicidad y escalabilidad | Lógica dispersa si el estado no está bien modelado |
| MVI | Eventos y estado único unidireccional | Predictibilidad en pantallas complejas | Exceso de abstracción en casos pequeños |
Qué patrón elegir
Para una aplicación Android nueva, MVVM suele ser el punto de partida más equilibrado. Funciona especialmente bien con componentes Jetpack y con Compose, permite estados observables y mantiene las pantallas más limpias.
Elige MVI cuando necesites un flujo de estado muy controlado, múltiples interacciones o una interfaz donde cada transición deba ser fácil de rastrear.
Mantén MVP cuando trabajes en una aplicación basada en XML con una arquitectura ya consolidada, o cuando tu equipo valore una separación muy explícita entre vista y lógica.
Usa MVC solo para casos pequeños o código legado sencillo. No es incorrecto, pero escala peor cuando la aplicación empieza a acumular reglas y estados.
La mejor arquitectura es la que permite que el equipo entienda una pantalla rápidamente, escriba pruebas con confianza y haga cambios sin romper comportamientos aparentemente ajenos.