Coroutines and Flow
What is a coroutine and how is it different from a thread?
A coroutine is a suspendable computation. When it waits (network, delay), it suspends and frees its thread for other coroutines instead of blocking it. Coroutines are managed by the Kotlin library, cost a few hundred bytes, and you can run hundreds of thousands of them on a small thread pool; threads are OS resources costing about 1 MB of stack each. Coroutines also give structured concurrency and built-in cancellation.
What does the suspend keyword do?
It marks a function that can pause without blocking a thread. The compiler transforms it into a state machine with an extra Continuation parameter (continuation-passing style), so it can resume where it stopped. A suspend function can only be called from another suspend function or a coroutine builder (launch, async, runBlocking). suspend does not move work to a background thread by itself; use withContext(Dispatchers.IO) for that.
What is the difference between launch and async?
Example
launch starts fire-and-forget work and returns a Job. async returns a Deferred<T>; call await() to get the result. Two async calls run concurrently, so total time is the slowest call, not the sum.
import kotlinx.coroutines.*
import kotlin.system.measureTimeMillis
suspend fun fetchPrice(): Int { delay(300); return 499 }
suspend fun fetchStock(): Int { delay(300); return 12 }
fun main(): Unit = runBlocking {
val job = launch { delay(100); println("launch: analytics event sent") }
val time = measureTimeMillis {
val price = async { fetchPrice() }
val stock = async { fetchStock() }
println("async: price=${price.await()}, stock=${stock.await()}")
}
job.join()
println("Both calls together took under 500 ms: ${time < 500}")
}Output
launch: analytics event sent async: price=499, stock=12 Both calls together took under 500 ms: true
What is structured concurrency?
Every coroutine runs inside a CoroutineScope, and child coroutines are tied to their parent: the parent waits for all children, cancelling the parent cancels all children, and a failing child cancels the parent and its siblings. This prevents leaked background work. On Android, viewModelScope and lifecycleScope cancel everything automatically when the ViewModel is cleared or the lifecycle is destroyed. Avoid GlobalScope.
Explain Dispatchers and withContext.
| Dispatcher | Use for |
|---|---|
Dispatchers.Main | UI updates on Android (main thread) |
Dispatchers.IO | Blocking I/O: files, JDBC, blocking network (pool of 64+ threads) |
Dispatchers.Default | CPU work: sorting, parsing JSON, image processing (threads = CPU cores) |
Dispatchers.Unconfined | Rarely; runs in the caller thread until first suspension |
withContext(Dispatchers.IO) { ... } switches the dispatcher for a block and returns its result. Repositories should be "main-safe": they switch to IO internally so callers can call them from the main thread.
coroutineScope vs supervisorScope: what happens when a child fails?
Example
In coroutineScope one failing child cancels all siblings and the exception is rethrown. In supervisorScope (and SupervisorJob, which viewModelScope uses) a child failure does not affect siblings; the failing child handles it with a CoroutineExceptionHandler or try/catch.
import kotlinx.coroutines.*
fun main(): Unit = runBlocking {
val handler = CoroutineExceptionHandler { _, e -> println("Handler caught: ${e.message}") }
supervisorScope {
launch(handler) { delay(100); throw IllegalStateException("payment widget failed") }
launch { delay(200); println("Order history loaded") }
}
try {
coroutineScope {
launch { delay(100); throw IllegalStateException("payment widget failed") }
launch { delay(200); println("never printed: sibling was cancelled") }
}
} catch (e: IllegalStateException) {
println("coroutineScope rethrew: ${e.message}")
}
}Output
Handler caught: payment widget failed Order history loaded coroutineScope rethrew: payment widget failed
How does cancellation work in coroutines?
Example
Cancellation is cooperative: job.cancel() only sets a flag, and the coroutine stops at the next suspension point (delay, yield, withContext) by throwing CancellationException. CPU loops must check isActive or call ensureActive(). Use finally for cleanup and never swallow CancellationException in a generic catch.
import kotlinx.coroutines.*
fun main(): Unit = runBlocking {
val job = launch {
try {
repeat(10) { i ->
println("Uploading chunk $i")
delay(100)
}
} finally {
println("Cleanup: closing connection")
}
}
delay(250)
job.cancelAndJoin()
println("Cancelled: ${job.isCancelled}")
val result = withTimeoutOrNull(150) { delay(1000); "done" }
println("withTimeoutOrNull result: $result")
}Output
Uploading chunk 0 Uploading chunk 1 Uploading chunk 2 Cleanup: closing connection Cancelled: true withTimeoutOrNull result: null
What is Flow and how is it different from a suspend function and from LiveData?
A suspend function returns one value; a Flow<T> emits many values over time, asynchronously. A plain flow { } is cold: the code runs again for every collector and only while collected. Compared with LiveData, Flow is pure Kotlin (no Android dependency, testable in plain JVM), has rich operators (map, combine, debounce, flatMapLatest) and controls threads with flowOn; it is not lifecycle-aware by itself, so collect it with repeatOnLifecycle or collectAsStateWithLifecycle.
Cold Flow vs StateFlow vs SharedFlow?
Example
StateFlow is a hot flow that always has a current value, replays the latest value to new collectors and skips equal consecutive values: ideal for UI state. SharedFlow is hot, has no required value and configurable replay: used for one-time events (navigation, snackbars). A cold flow restarts for each collector.
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun main(): Unit = runBlocking {
val cold = flow { println(" flow block started"); emit(1); emit(2) }
cold.collect { println(" A got $it") }
cold.collect { println(" B got $it") }
val state = MutableStateFlow(0)
val job = launch { state.collect { println("StateFlow collector got $it") } }
delay(50)
state.value = 1
delay(50)
state.value = 1 // equal value: not emitted again
delay(50)
state.value = 2
delay(50)
job.cancel()
println("Current value: ${state.value}")
}Output
flow block started A got 1 A got 2 flow block started B got 1 B got 2 StateFlow collector got 0 StateFlow collector got 1 StateFlow collector got 2 Current value: 2
How do flowOn and catch work?
Example
flowOn(dispatcher) changes the dispatcher of the operators above it (upstream); collection stays in the collector's context. catch handles exceptions from upstream only and can emit a fallback value. Never use withContext inside flow { } to emit.
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun main(): Unit = runBlocking {
(1..6).asFlow()
.filter { it % 2 == 0 }
.map { it * it }
.onEach { if (it > 20) throw IllegalArgumentException("too big: $it") }
.flowOn(Dispatchers.Default)
.catch { e -> println("caught ${e.message}"); emit(-1) }
.collect { println(it) }
}Output
4 16 caught too big: 36 -1