CoroutineScope es una interfaz de Kotlin que define el ciclo de vida de una corrutina y proporciona un contexto para lanzar nuevas corrutinas. Según la documentación de Kotlin, 2025, cada instancia de CoroutineScope contiene un CoroutineContext y gestiona todas las corrutinas lanzadas dentro de él. Cuando el scope se cancela, todas las corrutinas hijas se cancelan automáticamente, evitando fugas de memoria.
Puntos clave
CoroutineScope es una interfaz fundamental de la librería kotlinx.coroutines que sirve como contenedor para corrutinas. Define los límites del ciclo de vida de las corrutinas: cuando el scope se completa, todas las corrutinas dentro de él se cancelan automáticamente.
public interface CoroutineScope {
public val coroutineContext: CoroutineContext
}
La interfaz contiene solo un campo — coroutineContext. A través de él, el scope proporciona un despachador (Dispatcher), un trabajo (Job), un manejador de excepciones y otros elementos de contexto para todas las corrutinas lanzadas dentro de él.
Todas las funciones de lanzamiento de corrutinas — launch, async, runBlocking — son funciones de extensión en CoroutineScope. Esto significa que solo se pueden llamar cuando hay un objeto scope disponible. Este diseño garantiza que cada corrutina tenga un padre y un ciclo de vida claramente definidos.
En Android, cada componente arquitectónico tiene su propio scope: viewModelScope para ViewModel, lifecycleScope para Activity/Fragment. En aplicaciones de servidor, un scope puede estar vinculado a una solicitud HTTP o a un grupo de conexiones de base de datos.
Entender el funcionamiento interno de CoroutineScope requiere familiaridad con el concepto de Job y el principio de concurrencia estructurada.
Cada corrutina al lanzarse devuelve un objeto Job (o Deferred para async). Job representa una tarea con un ciclo de vida finito: New, Active, Completing, Completed, Cancelling, Cancelled. Los objetos Job forman una estructura de árbol:
Concurrencia estructurada es un principio arquitectónico clave de Kotlin Coroutines, donde el ciclo de vida de la corrutina está vinculado al ciclo de vida de su scope. Esto contrasta con el modelo “dispara y olvida”, donde una corrutina continúa viviendo después de que el scope se completa. Ventajas de la concurrencia estructurada:
Cuando se llama a scope.cancel(), el Job del scope transiciona al estado Cancelled, que cancela recursivamente todos los Jobs hijos. Después de la cancelación, el scope solo se puede reutilizar si se crea una nueva instancia de CoroutineScope.
Puedes crear un CoroutineScope mediante una función de fábrica o implementando la interfaz en tu clase. Veamos ambos enfoques.
val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
scope.launch {
println("Running on ${Thread.currentThread().name}")
}
La función de fábrica toma un CoroutineContext y crea un scope con el contexto especificado. El ejemplo usa Dispatchers.Default para tareas intensivas de CPU y SupervisorJob, que aísla las excepciones entre corrutinas hijas.
class MyRepository {
private val scope = CoroutineScope(Dispatchers.IO + Job())
suspend fun fetchData(): Data = scope.async {
api.getData()
}.await()
fun cleanup() {
scope.cancel()
}
}
Almacenamos el scope como un campo de clase y llamamos manualmente a cleanup para cancelarlo. Este enfoque es adecuado para componentes con un ciclo de vida gestionado, por ejemplo, repositorios o gestores.
Kotlin permite delegar la implementación de CoroutineScope mediante la palabra clave by:
class DataLoader : CoroutineScope by CoroutineScope(Dispatchers.IO) {
fun load() {
launch {
// coroutine runs in DataLoader scope
}
}
}
Este enfoque es conveniente cuando la clase misma es un scope y quiere proporcionar métodos de lanzamiento de corrutinas. Sin embargo, ten cuidado: la clase hereda todos los métodos de CoroutineScope, incluido cancel, lo que puede romper la encapsulación.
GlobalScope es un singleton de CoroutineScope para toda la aplicación. Su uso en código de producción no se recomienda oficialmente.
JetBrains permite GlobalScope solo en escenarios raros: procesos en segundo plano a nivel de aplicación que deben vivir incluso después de cerrar todas las Activities (por ejemplo, sincronización de datos, analítica). Pero incluso en estos casos, es preferible crear tu propio scope con CoroutineScope(SupervisorJob()).
Usa siempre un CoroutineScope personalizado con gestión explícita del ciclo de vida. En Android, estos son viewModelScope y lifecycleScope. En aplicaciones de servidor, crea un scope para cada solicitud o grupo de conexiones.
Ambas funciones son funciones suspend que crean un scope temporal para tareas paralelas, pero su comportamiento con excepciones difiere fundamentalmente.
| Característica | coroutineScope | supervisorScope |
|---|---|---|
| Comportamiento ante errores | Una excepción en una corrutina hija cancela todas las demás | Una excepción en una corrutina hija NO cancela las demás |
| Propagación de errores | Sí, la primera excepción se propaga hacia afuera | Sí, la primera excepción se propaga hacia afuera |
| Job por defecto | Job() — los hijos están vinculados al padre | SupervisorJob() — los hijos no dependen entre sí |
| Caso de uso típico | Operación atómica de varios pasos | Tareas paralelas independientes (cargas de UI) |
Usa coroutineScope cuando múltiples operaciones paralelas forman una única operación atómica. Por ejemplo, cargar datos de tres servidores: si una solicitud falla, las demás no tienen sentido.
suspend fun loadProductPage(): ProductPage = coroutineScope {
val product = async { api.getProduct() }
val reviews = async { api.getReviews() }
ProductPage(product.await(), reviews.await())
}
Si getProduct o getReviews lanzan una excepción — ambas corrutinas se cancelan y la excepción se propaga al código llamante.
Usa supervisorScope cuando las operaciones paralelas no dependen entre sí. Por ejemplo, cargar datos de perfil en varias secciones independientes: si la sección de recomendaciones falla, el encabezado del perfil y la lista de amigos deben mostrarse igualmente.
Veamos los errores más comunes de los desarrolladores al usar CoroutineScope en Kotlin.
El escenario más común de fuga de corrutinas es crear un scope sin llamar a cancel cuando el componente finaliza. Si el scope no se cancela, las corrutinas continúan ejecutándose, manteniendo referencias a objetos. En Android, usa viewModelScope o lifecycleScope, que se cancelan automáticamente.
GlobalScope ignora el ciclo de vida de los componentes de Android. Una corrutina lanzada en GlobalScope después de cerrar una Activity continuará ejecutándose e intentará actualizar la UI — lo que provoca un fallo. Usa siempre lifecycleScope para componentes de UI.
Después de llamar a cancel(), el scope no se puede reutilizar — todas las corrutinas dentro de él ya están completadas. Crea una nueva instancia de CoroutineScope mediante la función de fábrica. Job() no soporta reactivación.
Al delegar con by, la clase obtiene un método cancel() público que puede ser llamado desde cualquier lugar, rompiendo la encapsulación. Almacena el scope como un campo privado en lugar de delegar la interfaz.
Preguntas frecuentes
CoroutineScope es una interfaz que posee un CoroutineContext y es responsable del ciclo de vida de las corrutinas. CoroutineContext es un conjunto de elementos (despachador, job, manejador de errores) que define “cómo” se ejecuta una corrutina. Una diferencia: el scope crea corrutinas, mientras que el contexto controla su comportamiento.
Sí, es un patrón estándar: CoroutineScope(Dispatchers.IO + SupervisorJob()). SupervisorJob previene la cancelación en cascada de las corrutinas hijas cuando una de ellas lanza una excepción. Esto es útil para tareas paralelas independientes donde un error en una no debe detener a las demás.
No hay límite en la cantidad de corrutinas en un scope — solo están limitadas por la memoria disponible y la configuración del despachador. El límite práctico es típicamente de miles de corrutinas activas en un solo scope. Sin embargo, una gran cantidad de corrutinas puede indicar problemas arquitectónicos.
La forma correcta es pasar el scope a la clase mediante el constructor o usar runBlockingTest / runTest de kotlinx-coroutines-test. En las pruebas, puedes reemplazar el scope con TestCoroutineDispatcher y controlar la ejecución de las corrutinas manualmente.
No, un scope es un contenedor externo para una corrutina. La corrutina en sí misma no es un scope. Sin embargo, dentro de una corrutina puedes crear un nuevo scope mediante coroutineScope o supervisorScope para lanzar corrutinas hijas en paralelo.
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