Description
A Coroutine Scope is a structured boundary or environment within which one or more coroutines are launched, managed, and canceled in a predictable and hierarchical way. In languages like Kotlin, the coroutine scope provides essential lifecycle control for asynchronous operations and ensures that all child coroutines launched within a scope are tied to the same context.
It defines:
- Where a coroutine runs (via dispatcher),
- How long it lives (via lifecycle),
- What happens if an exception is thrown.
Without a proper coroutine scope, suspending functions cannot execute, and coroutine management becomes error-prone.
Why It Matters
Coroutine scopes enforce structured concurrency, meaning:
- Child coroutines automatically get canceled if their parent fails or is canceled.
- You can wait for all child coroutines to finish by suspending the parent scope.
- Scopes prevent “orphaned” coroutines that could leak memory or run indefinitely.
Core Properties
Property | Description |
---|---|
Lifecycle Bound | Scope can be tied to Android components, jobs, or manual cancellation |
Context-Aware | Scopes carry a CoroutineContext that defines behavior (e.g., thread, error) |
Structured | Child coroutines are automatically tracked and cancelled |
Basic Syntax (Kotlin)
CoroutineScope(Dispatchers.Default).launch {
// coroutine code here
}
Here, CoroutineScope
is created with a context (Dispatchers.Default
) and then a coroutine is launched within it using launch
.
Built-In Scopes in Kotlin
Scope | Purpose |
---|---|
GlobalScope | Lifetime of the entire application (use with caution) |
MainScope() | Default for UI-based apps like Android |
coroutineScope {} | Suspend function that creates a child scope |
supervisorScope {} | Ignores child coroutine failures (non-cascading cancellation) |
Custom Scope Example
val scope = CoroutineScope(Job() + Dispatchers.IO)
scope.launch {
val data = fetchData()
println(data)
}
This creates a reusable scope that can be canceled when needed.
coroutineScope vs CoroutineScope
Feature | coroutineScope | CoroutineScope |
---|---|---|
Type | Suspend function | Interface / builder |
Lifetime | Tied to the suspending call | Defined when instantiated |
Cancellation | Cancels entire block if any child fails | Manual control via Job() or lifecycle |
Integration with Lifecycle (Android Example)
class MyViewModel : ViewModel() {
private val viewModelScope = CoroutineScope(Dispatchers.Main + Job())
fun loadData() {
viewModelScope.launch {
val result = repository.getData()
_state.value = result
}
}
override fun onCleared() {
super.onCleared()
viewModelScope.cancel()
}
}
In Jetpack libraries, viewModelScope
and lifecycleScope
are provided for convenience.
CoroutineContext
Each coroutine scope has an associated CoroutineContext
made up of elements like:
- Dispatcher (e.g.,
Dispatchers.IO
,Dispatchers.Main
) - Job: Controls cancellation and lifecycle
- ExceptionHandler: Custom error propagation
Example:
val context = Job() + Dispatchers.Default + CoroutineName("DataFetch")
Scope Cancellation
You can cancel a scope to stop all child coroutines:
val scope = CoroutineScope(Job() + Dispatchers.IO)
scope.cancel()
Cancellation is cooperative, meaning the coroutine must check for cancellation using:
if (!isActive) return
Or use ensureActive()
to throw an exception if canceled.
Using supervisorScope
When you want child coroutines to operate independently:
supervisorScope {
launch { doSomething() }
launch { riskyTask() } // failure here won't cancel the others
}
Useful for tasks where some parts can fail without collapsing the whole operation.
Common Patterns
Launching in a Provided Scope
fun CoroutineScope.launchDataLoader() = launch {
val result = fetchData()
println(result)
}
Scoping for Timeout
withTimeout(5000) {
fetchData()
}
Using coroutineScope
in Suspended Functions
suspend fun fetchAndProcess() = coroutineScope {
val data = async { fetchData() }
val processed = async { processData() }
data.await() + processed.await()
}
Exception Propagation
In normal scopes:
- Any exception in a child coroutine cancels the entire scope.
In supervisorScope
:
- Exception in one child does not cancel others.
To handle exceptions explicitly:
val handler = CoroutineExceptionHandler { _, throwable ->
println("Caught: $throwable")
}
CoroutineScope(handler + Dispatchers.Default).launch {
throw RuntimeException("Boom!")
}
When to Use Each Scope
Scope | When to Use |
---|---|
GlobalScope | Rarely; only for truly global background tasks |
lifecycleScope | In Android components (Activity, Fragment) |
viewModelScope | For launching work from ViewModel |
coroutineScope {} | Inside suspend functions for safe and structured launch |
supervisorScope {} | When partial failure is acceptable and shouldn’t cancel siblings |
Lifecycle Management
You should cancel custom scopes manually when they are no longer needed:
val scope = CoroutineScope(Job())
...
scope.cancel() // Clean up
Failing to cancel scopes can cause leaks, background processing that never finishes, or unexpected errors.
Related Concepts
Concept | Connection |
---|---|
Coroutine Builder | Launches coroutines (launch , async ) inside a scope |
CoroutineContext | Defines the environment a scope operates within |
Job | Represents the lifecycle of the coroutine(s) in a scope |
Dispatcher | Specifies which thread or thread pool to use |
SupervisorJob | Enables independent failure behavior |
Structured Concurrency | Concept enabled and enforced by coroutine scopes |
Related Keywords
- Cancellation Token
- Coroutine Builder
- Coroutine Context
- Coroutine Dispatcher
- Coroutine Job
- Coroutine Lifecycle
- coroutineScope Function
- Global Scope
- Supervisor Scope
- ViewModel Scope