MVVM: qué es, el patrón Model-View-ViewModel en el desarrollo móvil

Autor: IT Sectr Publicado: 2026-02-16 Tiempo de lectura: 9 min

MVVM (Model-View-ViewModel) es un patrón arquitectónico en el que ViewModel reemplaza a Presenter y utiliza mecanismos reactivos para comunicarse con View: ObservableObject en SwiftUI, LiveData/StateFlow en Android. ViewModel no tiene referencia a View — los datos se transmiten mediante suscripción, lo que elimina la necesidad de interfaces ViewContract y simplifica aún más las pruebas. Apple recomienda MVVM con SwiftUI desde 2019, Google recomienda MVVM con Jetpack como la arquitectura oficial de Android. Más información en Android Architecture Guide.

Puntos clave

  • MVVM — Model (datos), View (interfaz), ViewModel (estado y lógica sin referencia a View)
  • Reactive binding — LiveData, StateFlow, ObservableObject actualizan automáticamente la UI al cambiar los datos
  • ViewModel — sobrevive a la rotación de pantalla y no depende de Android SDK/UIKit, se prueba con tests unitarios
  • Android Jetpack — ViewModel, LiveData, DataBinding — stack oficial de Google para MVVM
  • SwiftUI + Combine — implementación nativa de MVVM en iOS con @Published y @ObservedObject

Qué es MVVM: esencia del patrón Model-View-ViewModel

MVVM (Model-View-ViewModel) es un patrón arquitectónico descrito por John Gossman en 2005 para Windows Presentation Foundation (WPF) de Microsoft. ViewModel es el componente central que contiene el estado de la pantalla y la lógica de negocio, pero no tiene referencia a View. Los datos se transmiten mediante mecanismos de enlace reactivo: View se suscribe a los cambios de ViewModel y se vuelve a renderizar automáticamente cuando los datos cambian.

Diferencia clave entre MVVM y MVP — ausencia de ViewContract. En MVP, Presenter llama a métodos view.showUser(data), es decir, Presenter "empuja" activamente los datos a View. En MVVM, la propia View "tira" de los datos desde ViewModel mediante suscripción: ViewModel no sabe si tiene un suscriptor. Esto elimina el problema de View desconectada — si Activity se destruye al rotar, ViewModel continúa funcionando y la nueva Activity simplemente se suscribe a los datos actuales. En IT Sectr, usamos MVVM en todos los proyectos nuevos desde 2020 — el código se ha vuelto más predecible, las pruebas más estables.

ComponenteResponsabilidadPlataforma
ModelDatos, lógica de negocio, repositoriosAndroid/iOS
ViewVisualización, suscripción a ViewModelActivity/Composable, UIView/SwiftUI View
ViewModelEstado de pantalla, lógica, navegaciónViewModel (Jetpack), ObservableObject

Enlace reactivo — el fundamento de MVVM. En Android, LiveData (parte de Jetpack) es un contenedor de datos observable. Activity se suscribe mediante observe(): viewModel.user.observe(this) { user -> binding.name.text = user.name }. Cuando user cambia, todos los suscriptores reciben el nuevo valor automáticamente. En iOS, SwiftUI utiliza propiedades @Published en ViewModel — los cambios vuelven a renderizar View automáticamente. Esto elimina las llamadas manuales showUser/hideLoading requeridas en MVP.

MVVM en Android: ViewModel, LiveData y StateFlow

ViewModel de Jetpack — el componente oficial de Google para implementar MVVM. ViewModel sobrevive a la rotación de pantalla: cuando cambia la configuración, Activity se destruye y se recrea, mientras que ViewModel permanece en memoria. La nueva instancia de Activity obtiene el mismo ViewModel a través de ViewModelProvider. ViewModel no tiene referencias a Activity, Context o View — es limpio y se prueba con tests unitarios sin Robolectric.

kotlin
// ViewModel con StateFlow — implementación moderna de MVVM
class UserViewModel(
    private val repository: UserRepository
) : ViewModel() {

    private val _state = MutableStateFlow<UserState>(UserState.Loading)
    val state: StateFlow<UserState> = _state.asStateFlow()

    fun loadUser(userId: Int) {
        viewModelScope.launch {
            _state.value = UserState.Loading
            repository.getUser(userId)
                .onSuccess { user ->
                    _state.value = UserState.Success(user)
                }
                .onFailure { e ->
                    _state.value = UserState.Error(e.message ?: "Unknown")
                }
        }
    }
}

sealed interface UserState {
    data object Loading : UserState
    data class Success(val user: User) : UserState
    data class Error(val message: String) : UserState
}

// View (Activity) se suscribe a state
class UserActivity : AppCompatActivity() {
    private val viewModel: UserViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        viewModel.state.onEach { state ->
            when (state) {
                is UserState.Loading -> /* mostrar la carga */
                is UserState.Success -> /* mostrar los datos */
                is UserState.Error -> /* mostrar el error */
            }
        }.launchIn(lifecycleScope)
        viewModel.loadUser(42)
    }
}

LiveData vs StateFlow — LiveData (2017) — el primer componente reactivo de Jetpack, optimizado para el ciclo de vida de Activity: cancelación automática de suscripción en onStop. StateFlow (2021) — implementación de Kotlin Flow, no vinculada al ciclo de vida, pero que requiere cancelación manual mediante lifecycleScope. StateFlow admite corrutinas, concat, map y otros operadores de Flow, que LiveData no tiene. En IT Sectr, usamos StateFlow para todas las nuevas ViewModels — es más corto, más potente y se integra mejor con corrutinas.

DataBinding y ViewBinding — DataBinding vincula ViewModel con XML mediante @{viewModel.user.name} directamente en el diseño, eliminando código en Activity. ViewBinding genera una clase con seguridad de tipos para acceder a las vistas. Google recomienda ViewBinding para proyectos simples y DataBinding para proyectos con enlace de datos complejo. En Jetpack Compose, DataBinding no es necesario — las funciones @Composable se vuelven a renderizar automáticamente al cambiar el State.

MVVM en iOS: ObservableObject y SwiftUI

MVVM en iOS se implementa mediante ObservableObject de Combine. ViewModel es una clase que hereda ObservableObject, con propiedades @Published. SwiftUI View se suscribe a ViewModel mediante @ObservedObject o @StateObject. Cuando una propiedad @Published cambia, SwiftUI vuelve a renderizar automáticamente la View que depende de esta propiedad. Apple presentó SwiftUI en 2019 en la WWDC junto con Combine — desde entonces MVVM se ha convertido en el patrón oficialmente recomendado para iOS.

swift
import SwiftUI
import Combine

// ViewModel — ObservableObject con propiedades @Published
final class UserViewModel: ObservableObject {
    @Published private(set) var state: UserState = .loading
    private let service: UserService

    init(service: UserService) {
        self.service = service
    }

    func loadUser(id: Int) {
        state = .loading
        service.fetchUser(id: id) { [weak self] result in
            guard let self else { return }
            switch result {
            case .success(let user):
                self.state = .success(user)
            case .failure(let error):
                self.state = .error(error.localizedDescription)
            }
        }
    }
}

enum UserState {
    case loading
    case success(User)
    case error(String)
}

// SwiftUI View — se suscribe a ViewModel
struct UserView: View {
    @StateObject private var viewModel: UserViewModel

    var body: some View {
        switch viewModel.state {
        case .loading:
            ProgressView()
        case .success(let user):
            VStack {
                Text(user.name).font(.title)
                Text(user.email).font(.body)
            }
        case .error(let message):
            Text(message).foregroundColor(.red)
        }
    }
}

@StateObject vs @ObservedObject — @StateObject crea ViewModel y gestiona su ciclo de vida (una vez por vida de View). @ObservedObject — ViewModel se crea externamente y se pasa a View. WWDC 2022 recomienda @StateObject para la creación y @ObservedObject para pasar ViewModel entre Views. En iOS 17 (2023), apareció el macro @Observable — que automatiza la suscripción y elimina las anotaciones @Published. @Observable es la evolución de Combine, acercando el desarrollo de iOS a la reactividad de Kotlin Flow.

UIKit + MVVM — para proyectos con UIKit (sin SwiftUI), MVVM se implementa mediante Combine y @Published con suscripción en UIViewController a través de sink(). ViewModel es la misma, View es UIViewController con suscripciones a @Published. Combine está disponible desde iOS 13 (2019) y está integrado en el sistema — no requiere dependencias adicionales. Según Apple Developer Survey (2025), el 45% de los proyectos iOS usan Combine incluso con UIKit, el 35% usan SwiftUI + Combine, el 20% usan RxSwift (legado).

Comparación de MVVM con MVP: ventajas y desventajas

MVVM gana a MVP en tres aspectos clave: ausencia de interfaces ViewContract, gestión automática de suscripciones y supervivencia a la rotación de pantalla. En MVP, cada pantalla requiere una interfaz ViewContract + clase Presenter + suscripción/cancelación en onStart/onStop. En MVVM, solo se crea ViewModel — la suscripción en Activity se realiza mediante observe() sin detach() manual.

CriterioMVPMVVM
Interfaces ViewContract1 por pantallaNo necesarias
Gestión de suscripcionesattach/detach manualAutomática (lifecycle-aware)
Rotación de pantallaRetain-fragmentViewModel sobrevive a la rotación
PruebasMock ViewContractClase limpia sin dependencias
ReactividadCallbacks en PresenterLiveData/StateFlow/Combine

Desventajas de MVVM — complejidad de depurar cadenas reactivas y riesgo de fugas de memoria con suscripción incorrecta. LiveData resuelve la seguridad del ciclo de vida, StateFlow requiere lifecycleScope, Combine requiere sink con AnyCancellable. En MVP, todas las llamadas son explícitas (view.showUser), en MVVM los datos llegan a través de un flujo reactivo — la trazabilidad requiere puntos de interrupción en los closures subscribe. En ViewModels grandes con múltiples StateFlows, se puede perder una actualización de UI si View no está suscrita a un Flow específico.

Cuándo MVP sigue siendo mejor — en proyectos con versión mínima de Android inferior a API 21 (Android 5), donde Jetpack ViewModel no está disponible sin AndroidX, y en proyectos con UIKit puro sin Combine (iOS 12 y anteriores). Para proyectos heredados donde toda la base de código ya está en MVP, la transición completa a MVVM no siempre está justificada — es más barato mantener MVP con extracción gradual de lógica en servicios que reescribir 100 pantallas en 3 meses.

Pruebas de ViewModel en Android y iOS

ViewModel se prueba con tests unitarios sin dependencias de plataforma — este es el principal argumento a favor de MVVM. En Android, ViewModel no contiene Activity, Context o View — todas las dependencias (Repository, UseCase) se pasan mediante el constructor y se reemplazan con objetos mock. En iOS, ObservableObject se prueba mediante XCTest sin iniciar la aplicación, lo que proporciona estabilidad y velocidad de ejecución de pruebas.

kotlin
// Test unitario de ViewModel de Android con MockK
class UserViewModelTest {

    private val repository = mockk<UserRepository>()
    private val viewModel = UserViewModel(repository)

    @Test
    fun loadUser_success_updatesState() = runTest {
        val user = User(1, "John", "john@test.com")
        coEvery { repository.getUser(1) } returns Result.success(user)

        viewModel.loadUser(1)

        assertEquals(UserState.Success(user), viewModel.state.value)
    }

    @Test
    fun loadUser_error_updatesErrorState() = runTest {
        val error = RuntimeException("Network error")
        coEvery { repository.getUser(1) } returns Result.failure(error)

        viewModel.loadUser(1)

        val state = viewModel.state.value
        assertTrue(state is UserState.Error)
        assertEquals("Network error", (state as UserState.Error).message)
    }
}

ViewModel de iOS se prueba de manera similar: inyectamos mock UserService, llamamos loadUser, comprobamos el estado mediante XCTestExpectation. Combine Publisher se prueba mediante XCTestCase con wait(for: expectations, timeout: 1.0). La estructura UserState — enum con valores asociados — permite verificar el estado exacto de la pantalla después de una operación.

Cobertura de código en proyectos de IT Sectr con MVVM es del 75–90% para ViewModel y Repository. ViewModel se cubre con tests unitarios, Repository con tests de integración con base de datos de prueba. View en SwiftUI y Jetpack Compose se prueba con tests de UI (XCUITest, Compose Test) para escenarios críticos. El resto de la UI se verifica con tests de capturas de pantalla (Snapshot Testing) — es más rápido que los tests de UI y proporciona un 95% de confianza en la corrección de la visualización.

Preguntas frecuentes

¿Cuál es la diferencia principal entre MVVM y MVP?

En MVVM, ViewModel no tiene referencia a View — los datos se transmiten mediante mecanismos reactivos (LiveData, StateFlow, @Published). En MVP, Presenter llama directamente a los métodos de View a través de la interfaz ViewContract. MVVM elimina ViewContract y el attach/detach manual, pero requiere comprensión de los flujos reactivos. ViewModel sobrevive a la rotación de pantalla en Android, Presenter requiere un retain-fragment.

¿Qué bibliotecas se necesitan para MVVM en Android?

Conjunto mínimo: lifecycle-viewmodel-ktx (ViewModel), lifecycle-livedata-ktx o kotlinx-coroutines-core (StateFlow). Para inyección — Hilt o Koin. Para operaciones asíncronas — Kotlin Coroutines. Para enlace de datos complejo — DataBinding. En Jetpack Compose (recomendado por Google desde 2022), basta con compose-runtime y lifecycle-viewmodel-compose.

¿Por qué Apple recomienda MVVM para iOS?

SwiftUI (2019) está diseñado para arquitectura reactiva: @State y @Published vuelven a renderizar View automáticamente al cambiar los datos. MVVM es un ajuste natural para SwiftUI: View — @ViewBuilder, ViewModel — ObservableObject. Apple no impone MVVM como el único patrón, pero todos los materiales de formación desde 2019 usan ViewModel + SwiftUI. Para UIKit, Apple recomienda MVC o Coordinator.

¿Cómo evitar fugas de memoria en ViewModel?

Android: viewModelScope cancela automáticamente las corrutinas al limpiar ViewModel. iOS: AnyCancellable de Combine se cancela automáticamente al liberar el objeto que lo contiene. SwiftUI @StateObject gestiona el ciclo de vida automáticamente. Reglas principales: no almacenar referencias a View/Context en ViewModel, cancelar operaciones de larga duración al limpiar, usar weak self en closures.

¿Qué elegir: LiveData o StateFlow para Android?

StateFlow es la opción moderna. LiveData es más simple y seguro para el ciclo de vida, pero StateFlow es más potente: funciona con corrutinas, admite flatMap, combine, filter, no requiere anotación @Nullable. El único escenario donde LiveData es preferible — trabajar con código Java donde StateFlow (Kotlin Flow API) no está disponible. Google recomienda StateFlow para nuevos proyectos en Kotlin.

Resumen

  • MVVM (Model-View-ViewModel) — patrón reactivo donde ViewModel no tiene referencia a View
  • ViewModel — sobrevive a la rotación de pantalla, se prueba con tests unitarios, independiente de UI
  • Android — ViewModel + StateFlow + Kotlin Coroutines — stack moderno de Google
  • iOS — ObservableObject + @Published + SwiftUI — implementación nativa de MVVM
  • MVVM vs MVP — MVVM elimina ViewContract y el attach/detach manual
  • Pruebas — ViewModel se cubre con tests unitarios sin dependencias de plataforma
  • Recomendación — MVVM para proyectos nuevos; MVP para soporte de legado

Desarrollaremos una aplicación móvil llave en mano

IT Sectr crea aplicaciones para iOS y Android para startups y empresas desde 2017. Le asesoraremos y le propondremos la mejor solución.

Discutir el proyecto

Lea también