Kotlin Coroutine Scope
A coroutine scope defines the lifetime of coroutines. Every coroutine runs inside a scope. When the scope is cancelled or completes, all coroutines inside it are cancelled automatically. Scopes prevent coroutines from running indefinitely or leaking resources.
Why Scope Matters
Without scope:
launch { ... } // runs forever? who owns it? when does it stop?
With scope:
scope.launch { ... } // tied to scope's lifetime
scope.cancel() // all coroutines inside are cancelled
This is "structured concurrency" — no orphaned coroutines.
CoroutineScope
import kotlinx.coroutines.*
fun main() = runBlocking {
// coroutineScope waits for all child coroutines to finish
coroutineScope {
launch { delay(500); println("Task A") }
launch { delay(300); println("Task B") }
println("Scope started")
}
println("Scope finished — both tasks are done")
}Output:
Scope started
Task B
Task A
Scope finished — both tasks are donecoroutineScope vs runBlocking
runBlocking:
Blocks the thread until all coroutines inside complete.
Used to bridge regular code and coroutine code.
OK in main() and tests.
coroutineScope:
Suspends (not blocks) until all children complete.
Used inside suspend functions and other coroutines.
Does NOT block any thread.
Custom Scope with CoroutineScope
import kotlinx.coroutines.*
class DataManager {
private val scope = CoroutineScope(Dispatchers.Default)
fun startLoading() {
scope.launch {
delay(1000)
println("Data loaded on: ${Thread.currentThread().name}")
}
}
fun cleanup() {
scope.cancel() // cancels all running coroutines
println("Scope cancelled")
}
}
fun main() {
val manager = DataManager()
manager.startLoading()
Thread.sleep(1500) // wait for loading
manager.cleanup()
}Dispatchers
Dispatcher │ Thread Pool │ Best for
─────────────────────┼──────────────────────┼──────────────────────
Dispatchers.Default │ CPU cores │ heavy computation
Dispatchers.IO │ up to 64 threads │ file/network/database
Dispatchers.Main │ UI thread (Android) │ UI updates
Dispatchers.Unconfined│ caller's thread │ testing/special cases
import kotlinx.coroutines.*
fun main() = runBlocking {
launch(Dispatchers.Default) {
println("Default: ${Thread.currentThread().name}")
}
launch(Dispatchers.IO) {
println("IO: ${Thread.currentThread().name}")
}
delay(500)
}withContext — Switch Dispatcher
Use withContext to switch dispatchers inside a suspend function without creating a new coroutine:
suspend fun loadFile(path: String): String = withContext(Dispatchers.IO) {
println("Reading on: ${Thread.currentThread().name}")
delay(400) // simulate file read
"File content from $path"
}
suspend fun processData(data: String): String = withContext(Dispatchers.Default) {
println("Processing on: ${Thread.currentThread().name}")
data.uppercase()
}
fun main() = runBlocking {
val raw = loadFile("/data/report.txt")
val result = processData(raw)
println(result)
}Scope Hierarchy
runBlocking (root scope)
└── coroutineScope (child scope)
├── launch → child coroutine A
└── launch → child coroutine B
If coroutineScope is cancelled:
→ A is cancelled
→ B is cancelled
→ parent (runBlocking) continues
If child A throws an exception:
→ B is cancelled
→ coroutineScope propagates the exception to parent
Practical Example: Structured Data Fetcher
import kotlinx.coroutines.*
suspend fun fetchOrders(): List = withContext(Dispatchers.IO) {
delay(500)
listOf("Order #1001", "Order #1002", "Order #1003")
}
suspend fun fetchInventory(): Map = withContext(Dispatchers.IO) {
delay(400)
mapOf("Laptop" to 15, "Mouse" to 50, "Keyboard" to 30)
}
fun main() = runBlocking {
coroutineScope {
val ordersDeferred = async { fetchOrders() }
val inventoryDeferred = async { fetchInventory() }
val orders = ordersDeferred.await()
val inventory = inventoryDeferred.await()
println("Orders: $orders")
println("Inventory: $inventory")
}
println("Done")
} 