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 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.
// 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.
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.
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.
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.
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.
Cuando el sistema destruye la ViewModel, se llama a ViewModel.clear(). Dentro de clear(), ocurre lo siguiente:
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.
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.
| Capa | Componente | Rol de viewModelScope |
|---|---|---|
| UI | Activity / Fragment | Observa StateFlow/LiveData desde ViewModel |
| ViewModel | ViewModel | Lanza corrutinas mediante viewModelScope, gestiona el estado de la UI |
| Repository | Repository | Expone funciones suspend llamadas desde las corrutinas de viewModelScope |
| Data | DAO / Api | Ejecuta 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.
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.
Veamos tres escenarios prácticos de uso de viewModelScope en una aplicación Android con 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.
sealed class UiState {
object Loading : UiState()
data class Success(val data: List<Item>) : UiState()
data class Error(val message: String) : UiState()
}
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.
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.
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.
| Característica | viewModelScope | lifecycleScope |
|---|---|---|
| Propietario | ViewModel | LifecycleOwner (Activity/Fragment) |
| Cancelado en rotación | No (ViewModel sobrevive) | Sí (Activity se recrea) |
| Despachador por defecto | Dispatchers.Main.immediate | Dispatchers.Main.immediate |
| Disponible en | ViewModel | Activity, Fragment, Service |
| Caso de uso típico | Carga de datos, lógica de negocio | Interacciones de UI, animaciones, chunks |
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.
Incluso en una API de Android bien documentada, los desarrolladores cometen errores típicos. Veamos cuatro de los problemas más comunes.
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.
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.
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.
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
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.
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.
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.
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.
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
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.
Lea también