Why does an object with a lifecycle (e.g. a ViewModel, a service, a request handler) usually own its own CoroutineScope, and what does cancelling that scope accomplish?
answer
- Scope = Job + Dispatcher
- launch/async become children of the scope's Job
- scope.cancel() cancels Job -> all children stop
- Create on init, cancel on teardown
- GlobalScope outlives owner = leak
basics
~20 sThe object creates a scope to launch background work tied to its life. When the object is done, it cancels the scope, which stops every coroutine started in it so nothing keeps running and leaking.
solid answer
~40 sA CoroutineScope groups the coroutines an owner starts (via launch/async). Because of structured concurrency, every coroutine launched in the scope becomes a child of the scope's Job. When the owner reaches end-of-life, it calls scope.cancel(): this cancels the scope's Job, which propagates cancellation to all child coroutines, so in-flight work stops and resources (network calls, timers, collectors) are released. Without an owned scope you might use GlobalScope or a detached launch, and those coroutines outlive the owner — a leak. Owning the scope ties coroutine lifetime to object lifetime: create the scope when the owner is created, cancel it in the owner's teardown hook (onCleared, close(), @PreDestroy). Use CoroutineScope(SupervisorJob() + dispatcher) so one failing child does not tear down the others.
code
kotlin · 11 linesclass FeedService {
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
fun start() {
scope.launch { pollUpdates() } // child of scope's Job
}
fun shutdown() {
scope.cancel() // stops pollUpdates() and any sibling
}
}go deeper
Knows you create a scope, launch into it, and call scope.cancel() at the end to avoid leaks.
Explains the parent/child Job tree and that cancel() propagates downward to free resources; contrasts with GlobalScope.
Ties scope lifetime precisely to owner lifetime, picks SupervisorJob + dispatcher deliberately, and reasons about which teardown hook to cancel in.
Frames owned scopes as a leak-prevention and resource-ownership policy across a codebase, and standardizes the create/cancel discipline in shared infrastructure.
## What a CoroutineScope is A `CoroutineScope` is just an object that carries a `CoroutineContext` — most importantly a `Job` and a `CoroutineDispatcher`. Every coroutine builder (`launch`, `async`) is an extension on `CoroutineScope`, so calling `scope.launch { ... }` starts a coroutine *inside* that scope. The new coroutine's `Job` becomes a **child** of the scope's `Job`. ## Why an owner should hold a scope **Structured concurrency** means coroutines form a parent/child tree and a parent does not finish (and can be cancelled) along with its children. An object that starts background work — an Android `ViewModel`, a Spring service, a connection handler — has a clear lifetime. By giving it its own scope, every coroutine it starts is anchored to that lifetime. - **Create** the scope when the owner is created: ```kotlin private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) ``` - **Use** it to launch work: `scope.launch { repository.refresh() }`. - **Cancel** it in the owner's teardown: `scope.cancel()`. ## What scope.cancel() does `scope.cancel()` cancels the scope's `Job`. Cancellation **propagates downward** to every child coroutine: each child's `Job` is cancelled, suspension points throw `CancellationException`, and the coroutines stop at their next cancellation check. This frees whatever they held — open sockets, database cursors, `Flow` collectors, delays. This is the core leak-prevention mechanism: one call shuts down a whole subtree. ## What a leak looks like without it If you instead use `GlobalScope.launch { ... }`, the coroutine is tied to the *application*, not your object. When your object is destroyed the coroutine keeps running, holding references (often the destroyed object itself), never to be cleaned up. That is a coroutine leak. ## Key APIs/keywords - `CoroutineScope(...)` — factory that builds a scope from a context. - `SupervisorJob()` — a `Job` where a child's failure does not cancel siblings. - `Dispatchers.Default` / `Dispatchers.IO` / `Dispatchers.Main` — the dispatcher choosing the thread pool. - `scope.cancel()` — cancels the scope's Job and all children. - `launch` / `async` — builders that create child coroutines. The rule of thumb: *whoever creates the scope is responsible for cancelling it.*
- What happens to coroutines you launched if you forget to call scope.cancel()?They keep running (or stay suspended) and hold references to the owner, leaking memory and possibly doing useless work; nothing automatically stops them.
- Who is responsible for cancelling the scope?The component that created/owns the scope, in its lifecycle teardown hook (e.g. onCleared(), close(), @PreDestroy).
The scope is a power strip: plug devices (coroutines) into it; flip the strip's switch (cancel) and everything plugged in turns off at once.
saying these in an interview costs you the question
- Thinks coroutines stop on their own when the object is garbage-collected
- Defaults to GlobalScope for object-scoped work
- Cannot explain that cancel() propagates to children
- Confuses creating a scope with creating a thread
- Never cancels the scope anywhere