← Volver al portafolio
// Android Dev · Arquitectura · Kotlin

Por qué MVVM es el estándar de oro en Android: guía práctica con Kotlin

Por GavDevCR · Junio 2026 · Lectura de 7 min

Si llevás tiempo desarrollando para Android, seguramente habrás escuchado sobre MVVM. Pero más allá de ser una sigla que aparece en la documentación de Google, es la diferencia entre una app que escala y una que se convierte en un laberinto de código ilegible. En este artículo te explico por qué MVVM es el patrón arquitectónico que deberías usar en todos tus proyectos Android con Kotlin, y cómo implementarlo paso a paso.

¿Qué es MVVM y por qué importa?

MVVM significa Model-View-ViewModel. Es un patrón de diseño que separa la aplicación en tres capas con responsabilidades bien definidas. El objetivo principal es uno solo: ninguna capa debe saber demasiado sobre las otras.

📱 View (Composables / Activities) ↕ observa estado / envía eventos 🧠 ViewModel (lógica de UI) ↕ solicita / recibe datos 📦 Repository (fuente de verdad) ↕ consulta / escribe 🗄️ Room DB  |  🌐 API REST

La View (tu Activity, Fragment o Composable) solo se encarga de mostrar información y capturar acciones del usuario. El ViewModel procesa esas acciones, aplica lógica y expone el estado actualizado. El Model (Repository + fuentes de datos) se encarga de obtener y persistir la información, sin importarle quién la va a mostrar.

1. El ViewModel: donde vive la lógica de UI

El ViewModel es el componente más importante del patrón. Sobrevive los cambios de configuración del dispositivo (como rotar la pantalla), lo que significa que el usuario no pierde su estado al girar el teléfono. Usa StateFlow para exponer el estado de forma reactiva.

// ViewModel básico con StateFlow class TareasViewModel(private val repo: TareasRepository) : ViewModel() { // Estado privado (mutable, solo el ViewModel lo modifica) private val _uiState = MutableStateFlow(TareasUiState()) // Estado público (solo lectura para la View) val uiState: StateFlow<TareasUiState> = _uiState.asStateFlow() init { cargarTareas() } private fun cargarTareas() { viewModelScope.launch { repo.getTareas().collect { tareas -> _uiState.update { it.copy(tareas = tareas, isLoading = false) } } } } fun completar(tarea: Tarea) { viewModelScope.launch { repo.marcarCompletada(tarea.id) } } } // Data class para representar el estado de la UI data class TareasUiState( val tareas: List<Tarea> = emptyList(), val isLoading: Boolean = true, val error: String? = null )

2. La View: solo observa y reacciona

Con Jetpack Compose, la View se vuelve declarativa: describís qué mostrar según el estado, no cómo actualizar la pantalla. El sistema de recomposición de Compose se encarga de actualizar solo lo que cambió.

// Composable que observa el ViewModel @Composable fun TareasScreen(viewModel: TareasViewModel = viewModel()) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() when { uiState.isLoading -> CircularProgressIndicator() uiState.error != null -> ErrorMessage(uiState.error!!) else -> TareasList( tareas = uiState.tareas, onCompletar = viewModel::completar ) } }

collectAsStateWithLifecycle vs collectAsState

Siempre usá collectAsStateWithLifecycle() en lugar de collectAsState(). La primera respeta el ciclo de vida de Android y pausa la recolección cuando la app está en segundo plano, ahorrando batería y recursos del sistema.

3. El Repository: la única fuente de verdad

El Repository actúa como intermediario inteligente. El ViewModel no sabe si los datos vienen de una base de datos local, una API remota o una caché. Esa complejidad vive en el Repository, y así el código de UI permanece limpio.

// Repository que combina Room DB y API remota class TareasRepository( private val dao: TareasDao, private val api: TareasApiService ) { // Siempre retorna los datos locales primero (offline-first) fun getTareas(): Flow<List<Tarea>> = dao.getAll() suspend fun sincronizar() { try { val remotas = api.getTareas() dao.insertAll(remotas.map { it.toEntity() }) } catch (e: IOException) { // Sin internet: los datos locales siguen funcionando } } suspend fun marcarCompletada(id: Int) { dao.update(id, completada = true) } }

4. Errores comunes que hay que evitar

5. ¿Vale la pena toda esta estructura para apps pequeñas?

Sí, y te explico por qué: el costo de configurar MVVM correctamente desde el inicio es mínimo comparado con refactorizar una app enredada semanas después. Además, cuando tu app crezca, agregar una nueva pantalla o funcionalidad será un proceso predecible y limpio en lugar de un rompecabezas.

Aplicar estos patrones en Flujo, mi app de finanzas personales, fue la decisión más importante del proyecto. Me permitió iterar rápidamente, agregar nuevas categorías de gasto sin tocar la UI, y mantener la lógica del balance calculado en un solo lugar confiable.

Regla de oro: si no podés hacer un test unitario sin instanciar la UI, tu arquitectura necesita trabajo

Una buena implementación de MVVM te permite escribir tests para el ViewModel y el Repository sin ningún componente de Android. Eso es código limpio, mantenible y profesional.