viewModelScope: qué es, vinculación con ViewModel y cómo funciona en Android

Autor: IT Sectr Publicado: 2026-06-23 Tiempo de lectura: 9 min

viewModelScope es un CoroutineScope incorporado de la biblioteca androidx.lifecycle que está vinculado al ciclo de vida de ViewModel y se cancela automáticamente cuando se limpia. Según Google Android Developers, 2025, viewModelScope es el mecanismo estándar para lanzar corrutinas en la arquitectura MVVM, garantizando operaciones asíncronas seguras sin riesgo de fugas de memoria. ViewModelScope usa Dispatchers.Main por defecto, y todas las operaciones de IO dentro de él deben ejecutarse mediante withContext.

Puntos clave

  • viewModelScope — CoroutineScope de lifecycle-viewmodel-ktx, cancelado cuando se llama a ViewModel.onCleared()
  • Dispatchers.Main — despachador por defecto, por lo que las actualizaciones de la UI dentro de las corrutinas son seguras
  • onCleared — callback que activa la cancelación automática de todas las corrutinas activas en viewModelScope
  • clear() vs onCleared() — clear() es llamado por el framework antes de onCleared, garantizando la cancelación del scope
  • launch — la forma principal de iniciar corrutinas en viewModelScope para operaciones fire-and-forget

¿Qué es viewModelScope en Android?

viewModelScope es una propiedad de extensión en la interfaz ViewModel, añadida en la biblioteca lifecycle-viewmodel-ktx (a partir de la versión 2.1.0). Proporciona un CoroutineScope listo para usar vinculado al ciclo de vida de ViewModel.

kotlin
// Internal structure (simplified)
val ViewModel.viewModelScope: CoroutineScope
    get() {
        val scope = this.getTag(JOB_KEY)
        if (scope != null) return scope
        return CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
            .also { setTag(JOB_KEY, it) }
    }

El scope se crea de forma perezosa (lazy) en el primer acceso y se almacena en caché mediante setTag. Utiliza SupervisorJob, lo que significa que una excepción en una corrutina hija no cancela las demás. El despachador por defecto es Dispatchers.Main.immediate, que ejecuta código en el hilo principal sin despacho adicional si ya está en Main.

Cómo recibe viewModelScope la notificación de limpieza

Cuando la ViewModel abandona el ciclo de vida (la Activity finaliza o se elimina el Fragment), el sistema llama a clear(), que activa onCleared(). En este callback, viewModelScope cancela su Job, lo que finaliza recursivamente todas las corrutinas activas. El mecanismo se implementa a través de la interfaz Closeable, donde el Job del scope se registra como un recurso para cierre automático.

Cómo funciona viewModelScope: vinculación con el ciclo de vida de ViewModel

El mecanismo de vinculación de viewModelScope al ciclo de vida de ViewModel se basa en el etiquetado y el callback onCleared. Veámoslo paso a paso.

Paso 1: Creación del scope en el primer acceso

Cuando la ViewModel ejecuta viewModelScope.launch { ... }, el getter comprueba si ya hay un scope almacenado bajo la etiqueta JOB_KEY. Si no existe, se crea una nueva instancia de CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate). El scope se almacena dentro de la ViewModel mediante un mapa interno de etiquetas.

Paso 2: Ciclo de vida de las corrutinas

Todas las corrutinas lanzadas mediante viewModelScope.launch o viewModelScope.async se convierten en hijas del SupervisorJob del scope. Se ejecutan en el hilo principal (a menos que se especifique otro despachador mediante withContext). Mientras la ViewModel esté viva, las corrutinas pueden estar activas, suspendidas o completadas.

Paso 3: Cancelación en onCleared

Cuando el sistema destruye la ViewModel, se llama a ViewModel.clear(). Dentro de clear(), ocurre lo siguiente:

  • Se llama a onCleared() para la lógica de limpieza personalizada
  • Todos los recursos Closeable registrados mediante addCloseable se cierran
  • El Job de viewModelScope pasa al estado Cancelled
  • Todas las corrutinas hijas se cancelan recursivamente
  • Las referencias al scope se liberan para el recolector de basura

Resistencia a la rotación

Cuando la pantalla se rota, la Activity se recrea, pero la ViewModel sobrevive (gracias a ViewModelStoreOwner). Esto significa que viewModelScope permanece activo y las corrutinas continúan ejecutándose sin interrupción. Después de recrear la Activity, se reutiliza la misma ViewModel (y el mismo scope) — la carga de datos no comienza desde cero.

viewModelScope en la arquitectura MVVM

MVVM (Model-View-ViewModel) es la arquitectura recomendada por Google para aplicaciones Android. viewModelScope ocupa un lugar central en ella como ejecutor de operaciones asíncronas.

El rol de viewModelScope en las capas de la arquitectura

CapaComponenteRol de viewModelScope
UIActivity / FragmentObserva StateFlow/LiveData desde ViewModel
ViewModelViewModelLanza corrutinas mediante viewModelScope, gestiona el estado de la UI
RepositoryRepositoryExpone funciones suspend llamadas desde las corrutinas de viewModelScope
DataDAO / ApiEjecuta las solicitudes reales (Room, Retrofit)

La ViewModel lanza corrutinas mediante viewModelScope, dentro de las cuales llama a funciones suspend del Repository. El resultado se transforma en StateFlow, que es observado por la capa de UI. Este diseño garantiza una clara separación de responsabilidades y la capacidad de prueba independiente de cada capa.

Por qué viewModelScope en ViewModel y no en Fragment

Si las corrutinas se lanzaran desde un Fragment, se cancelarían al rotar la pantalla junto con la destrucción del Fragment. La ViewModel sobrevive a la rotación, por lo que las corrutinas lanzadas en su scope continúan ejecutándose. Esta es la ventaja clave de viewModelScope sobre lifecycleScope al cargar datos.

Ejemplos de uso de viewModelScope

Veamos tres escenarios prácticos de uso de viewModelScope en una aplicación Android con Kotlin.

Ejemplo 1: Carga de datos al crear la ViewModel

kotlin
class ProfileViewModel(
    private val repo: ProfileRepository
) : ViewModel() {

    private val _profile = MutableStateFlow<Profile?>(null)
    val profile: StateFlow<Profile?> = _profile

    init {
        loadProfile()
    }

    private fun loadProfile() {
        viewModelScope.launch {
            val result = repo.getProfile()
            _profile.value = result
        }
    }
}

En el bloque init, la carga del perfil comienza inmediatamente. La corrutina se ejecuta en el hilo principal (por defecto). El repositorio usa withContext(Dispatchers.IO) para la solicitud de red dentro de su función suspend, por lo que la ViewModel no necesita preocuparse por el cambio de hilos.

Ejemplo 2: Manejo de errores con sealed class

kotlin
sealed class UiState {
    object Loading : UiState()
    data class Success(val data: List<Item>) : UiState()
    data class Error(val message: String) : UiState()
}
kotlin
fun fetchItems() {
    _state.value = UiState.Loading
    viewModelScope.launch {
        try {
            val items = repo.getItems()
            _state.value = UiState.Success(items)
        } catch (e: Exception) {
            _state.value = UiState.Error(e.message ?: "Unknown error")
        }
    }
}

El estado de la UI se describe mediante una sealed class UiState. La ViewModel actualiza el estado en cada cambio. El Fragment se suscribe a StateFlow y reacciona solo al estado actual, ignorando llamadas obsoletas de rotaciones anteriores.

Ejemplo 3: Cancelación de la corrutina anterior en una nueva solicitud

kotlin
private var searchJob: Job? = null

fun search(query: String) {
    searchJob?.cancel()
    searchJob = viewModelScope.launch {
        delay(300)
        val results = repo.search(query)
        _searchResults.value = results
    }
}

En cada nueva consulta de búsqueda, la corrutina anterior se cancela. delay(300) implementa debounce — la búsqueda se ejecuta solo después de 300 ms de inactividad. Esto reduce la carga del servidor y evita resultados obsoletos.

viewModelScope vs lifecycleScope: cuándo elegir cada uno

Ambos scopes son proporcionados por la biblioteca AndroidX Lifecycle, pero están vinculados a diferentes ciclos de vida. La elección depende del tipo de tarea.

Comparación de scopes

CaracterísticaviewModelScopelifecycleScope
PropietarioViewModelLifecycleOwner (Activity/Fragment)
Cancelado en rotaciónNo (ViewModel sobrevive)Sí (Activity se recrea)
Despachador por defectoDispatchers.Main.immediateDispatchers.Main.immediate
Disponible enViewModelActivity, Fragment, Service
Caso de uso típicoCarga de datos, lógica de negocioInteracciones de UI, animaciones, chunks

Recomendaciones de Google

Google recomienda usar viewModelScope para todas las tareas de carga y procesamiento de datos. lifecycleScope debe usarse para operaciones vinculadas a un momento específico del ciclo de vida de la UI — por ejemplo, iniciar una animación en la primera aparición de la pantalla o suscribirse a actualizaciones de ubicación que deben detenerse al salir de la pantalla.

Errores comunes al trabajar con viewModelScope

Incluso en una API de Android bien documentada, los desarrolladores cometen errores típicos. Veamos cuatro de los problemas más comunes.

Error 1: Actualizar la UI después de la cancelación del scope

El error más insidioso es intentar actualizar StateFlow o LiveData después de que la ViewModel haya sido limpiada. Aunque viewModelScope se cancela en onCleared(), una corrutina puede ejecutar código antes de que la cancelación surta efecto. Usa isActive para verificar o confía en la finalización del bloque catch.

Error 2: Lanzar corrutinas sin considerar SupervisorJob

viewModelScope usa SupervisorJob internamente, lo que aísla los errores entre corrutinas. Sin embargo, si lanzas una corrutina con su propio Job() dentro de viewModelScope.launch, esa corrutina se convierte en hija de SupervisorJob pero no estará protegida contra la cancelación causada por errores en otras corrutinas.

Error 3: Demasiadas corrutinas en un mismo scope

Aunque viewModelScope no tiene un límite estricto, miles de corrutinas activas pueden ralentizar el sistema. Para listas largas de datos, usa Flow con collectLatest en lugar de crear corrutinas separadas para cada elemento.

Error 4: Usar GlobalScope en lugar de viewModelScope

Si se importa accidentalmente GlobalScope en lugar de viewModelScope, la corrutina no se cancelará al limpiar la ViewModel. Esto provoca fugas de memoria y posibles fallos. Asegúrate siempre de que las corrutinas se lancen mediante viewModelScope, especialmente en subclases de Fragment.

Preguntas frecuentes

¿Puedo cambiar el despachador por defecto de viewModelScope?

No puedes cambiar directamente el despachador de viewModelScope — está fijado como Dispatchers.Main.immediate. Sin embargo, dentro de una corrutina puedes cambiar a otro despachador mediante withContext. Para cambiar el despachador en pruebas, usa TestDispatcher a través de una Rule.

¿Cómo paso viewModelScope al Repository?

No pases el scope al Repository — esto rompe los principios arquitectónicos. El Repository debe exponer funciones suspend, y la ViewModel misma gestiona las corrutinas mediante viewModelScope. Si el Repository requiere un scope, reconsidera la arquitectura en favor de Clean Architecture.

¿Por qué viewModelScope usa SupervisorJob?

SupervisorJob garantiza que una excepción en una corrutina (por ejemplo, un error de carga en una de varias solicitudes independientes) no cancele las demás corrutinas. Esto coincide con el escenario de ViewModel, donde diferentes pantallas cargan datos independientes.

¿Está disponible viewModelScope en Jetpack Compose?

Sí, viewModelScope está disponible en cualquier ViewModel independientemente del tipo de UI (View System o Jetpack Compose). En Compose, las corrutinas también se lanzan mediante viewModelScope, mientras que para efectos de UI se usan LaunchedEffect y rememberCoroutineScope.

¿Qué sucede con una corrutina cuando se llama a viewModelScope.cancel()?

Llamar a viewModelScope.cancel() cancela el scope inmediatamente — todas las corrutinas activas terminan con CancellationException. Si después se llama a viewModelScope.launch, se crea un nuevo scope automáticamente en el siguiente acceso al getter.

Resumen

  • viewModelScope — CoroutineScope vinculado al ciclo de vida de ViewModel, cancelado automáticamente en onCleared()
  • SupervisorJob + Dispatchers.Main — configuración interna que garantiza aislamiento de errores y acceso seguro a la UI
  • Rotación de pantalla — la ViewModel sobrevive, por lo que las corrutinas en viewModelScope continúan sin reiniciarse
  • Arquitectura MVVM — viewModelScope es el elemento central para operaciones asíncronas en la capa de ViewModel
  • lifecycleScope — alternativa para operaciones vinculadas al ciclo de vida de Activity/Fragment, no a ViewModel
  • StateFlow — forma preferida de pasar datos desde las corrutinas de viewModelScope a la UI mediante sealed class
  • GlobalScope es peligroso — reemplazar viewModelScope por GlobalScope provoca fugas de memoria y fallos de la aplicación

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