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

Leave a Comment

Your email address will not be published. Required fields are marked *