Kotlin Coroutines Advanced
This topic covers structured concurrency, cancellation, exception handling, Flows, and Channels — the features you will use in real Android and backend applications.
Structured Concurrency
Structured concurrency means every coroutine has a parent. When the parent is cancelled, all its child coroutines cancel automatically. This prevents memory leaks and runaway tasks.
fun main() = runBlocking {
val parent = launch {
launch { delay(5000); println("Child 1 done") }
launch { delay(3000); println("Child 2 done") }
delay(1000)
println("Parent finishing early")
}
parent.cancelAndJoin() // cancels parent AND both children
println("All stopped")
}
// Parent finishing early
// All stopped
// (neither child prints)
Cancellation
Coroutines are cooperative — they cancel only at suspension points (delay, yield, or any suspend function). CPU-heavy loops must check cancellation manually.
val job = launch {
repeat(1000) { i ->
if (!isActive) return@launch // cooperate with cancellation
println("Working $i")
delay(100)
}
}
delay(350)
job.cancel()
job.join()
println("Cancelled")
withTimeout — Cancel After a Time Limit
try {
withTimeout(2000L) {
repeat(10) {
delay(500)
println("Step $it")
}
}
} catch (e: TimeoutCancellationException) {
println("Timed out!")
}
// Step 0, Step 1, Step 2, Timed out!
// withTimeoutOrNull returns null instead of throwing:
val result = withTimeoutOrNull(1000L) {
delay(2000)
"Done"
}
println(result) // null
Exception Handling in Coroutines
CoroutineExceptionHandler
val handler = CoroutineExceptionHandler { _, exception ->
println("Caught: ${exception.message}")
}
val scope = CoroutineScope(Dispatchers.Default + handler)
scope.launch {
throw RuntimeException("Something went wrong!")
}
// Caught: Something went wrong!
SupervisorJob — Isolate Child Failures
With a regular Job, one child failure cancels the entire parent and all siblings. A SupervisorJob lets other children continue when one fails.
val supervisor = SupervisorJob()
val scope = CoroutineScope(Dispatchers.Default + supervisor)
scope.launch {
throw RuntimeException("Child 1 failed")
}
scope.launch {
delay(500)
println("Child 2 still running") // runs despite child 1 failing
}
Diagram — Job vs SupervisorJob
Regular Job: SupervisorJob: Parent Parent ├── Child 1 (fails) ──→ ├── Child 1 (fails) ──→ only this cancels ├── Child 2 (cancelled) ├── Child 2 (keeps running) └── Child 3 (cancelled) └── Child 3 (keeps running)
Kotlin Flow — Asynchronous Streams
A Flow emits multiple values over time — like a stream of data. A suspend function returns one value. A Flow returns many values, one by one.
import kotlinx.coroutines.flow.* fun numberStream(): Flow= flow { for (i in 1..5) { delay(300) emit(i) // emit sends one value downstream } } fun main() = runBlocking { numberStream().collect { value -> println("Received: $value") } } // Received: 1 // Received: 2 // ... (every 300ms) // Received: 5
Flow Operators
fun main() = runBlocking {
(1..10).asFlow()
.filter { it % 2 == 0 } // keep even numbers
.map { it * it } // square them
.take(3) // take first 3
.collect { println(it) } // 4, 16, 36
}
Cold vs Hot Flow
Cold Flow: Hot Flow (StateFlow / SharedFlow): Starts when collected. Runs even without a collector. Each collector gets its own run. Multiple collectors share one run. Example: file reading, API call Example: UI state, events
StateFlow — Observable State
StateFlow holds a value and emits updates. It is widely used in Android ViewModel to expose UI state.
class CounterViewModel : ViewModel() {
private val _count = MutableStateFlow(0)
val count: StateFlow = _count.asStateFlow()
fun increment() { _count.value++ }
}
// In UI (Fragment/Activity):
viewModel.count.collect { value ->
textView.text = value.toString()
}
Channel — Coroutine Communication
A Channel sends data from one coroutine to another — like a pipe. The sender puts data in; the receiver takes it out. The channel can optionally buffer values.
fun main() = runBlocking {
val channel = Channel()
launch {
for (x in 1..5) {
channel.send(x)
println("Sent: $x")
}
channel.close()
}
for (value in channel) {
println("Received: $value")
}
}
Diagram — Channel as a Pipe
Coroutine A (Producer) Channel Coroutine B (Consumer) send(1) ──────────────────→ [1][2][3] ─────────────────→ receive() = 1 send(2) receive() = 2 send(3) receive() = 3 close() closed ─────────────────→ loop ends
