PPrepLearn
Progress Β· 0/10 sections

πŸ”΅ πŸ”΅ Coroutines & Kotlin Flows

5 min read Β· Notion


⚑ Coroutines Fundamentals

What are Kotlin Coroutines?

Coroutines are lightweight concurrency primitives β€” suspendable computations that can be paused and resumed without blocking a thread. They're built on top of continuation-passing style (CPS) transforms at the compiler level.

  • 1 thread can run thousands of coroutines.
  • No new threads created for suspend function calls.
  • The compiler transforms suspend functions into state machines.
launch vs async
launchasync
ReturnsJobDeferred<T>
PurposeFire-and-forgetCompute a value concurrently
ResultNo result.await() to get result
ExceptionPropagates to parentSurfaces on .await()
val job = launch { doWork() }
val deferred = async { computeValue() }
val result = deferred.await()
What is a suspend function?

A function marked with suspend can be paused and resumed. It can only be called from a coroutine or another suspend function. Under the hood, the compiler adds a Continuation parameter to manage the state machine.

suspend fun fetchUser(): User {
    return withContext(Dispatchers.IO) {
        api.getUser() // network call, doesn't block thread
    }
}
Coroutine Dispatchers
DispatcherUse Case
Dispatchers.MainUI updates, LiveData observation
Dispatchers.IONetwork, database, file I/O
Dispatchers.DefaultCPU-intensive work (sorting, parsing)
Dispatchers.UnconfinedTesting; not recommended for production
withContext(Dispatchers.IO) { /* I/O work */ }
withContext(Dispatchers.Default) { /* CPU work */ }
What is structured concurrency?

Structured concurrency guarantees that coroutines launched in a scope are bound to that scope's lifetime:

  • When a scope is cancelled, all its children are cancelled.
  • A parent won't complete until all its children complete.
  • Exceptions propagate upward through the hierarchy.
viewModelScope.launch {
    val a = async { fetchA() }
    val b = async { fetchB() }
    process(a.await(), b.await())
} // Both a and b cancelled if viewModel is cleared
CoroutineScope: viewModelScope, lifecycleScope, GlobalScope
  • viewModelScope: Tied to ViewModel. Cancelled when ViewModel is cleared. Best for data loading.
  • lifecycleScope: Tied to Activity/Fragment lifecycle. Use for UI-bound work.
  • GlobalScope: App-lifetime scope. Avoid β€” leaks memory, breaks structured concurrency.
Exception handling in coroutines
// Option 1: try-catch inside coroutine
launch {
    try { riskyCall() }
    catch (e: Exception) { handleError(e) }
}
 
// Option 2: CoroutineExceptionHandler (for launch only)
val handler = CoroutineExceptionHandler { _, throwable ->
    Log.e("Error", throwable.message ?: "")
}
launch(handler) { riskyCall() }

async exceptions are NOT caught by CoroutineExceptionHandler β€” they surface on .await().

SupervisorJob vs Job
  • Job: Failure in one child cancels all siblings.
  • SupervisorJob: Each child fails independently; siblings are not cancelled.
val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
scope.launch { failingTask() }   // doesn't cancel other children
scope.launch { otherTask() }     // continues running

ViewModelScope uses SupervisorJob internally.


🌊 Kotlin Flow

What is Kotlin Flow?

Flow is a cold, asynchronous stream of values built on coroutines. "Cold" means the stream doesn't start until it's collected.

fun getNumbers(): Flow<Int> = flow {
    for (i in 1..5) {
        delay(100)
        emit(i)
    }
}
 
viewModelScope.launch {
    getNumbers().collect { println(it) }
}
Flow vs StateFlow vs SharedFlow
FlowStateFlowSharedFlow
Cold/HotColdHotHot
Initial valueNoRequiredNo
Replays0Latest (1)Configurable
CollectorsIndependent streamsSharedShared
Use caseOne-shot streamsUI stateEvents
val uiState: StateFlow<UiState> = MutableStateFlow(UiState.Loading)
val events: SharedFlow<Event> = MutableSharedFlow()
Flow operators: map, filter, transform, debounce, collectLatest
flow
    .filter { it > 2 }
    .map { it * 10 }
    .debounce(300L)        // wait 300ms of silence before emitting
    .collectLatest { value ->
        // cancels previous block if new value arrives
        expensiveOperation(value)
    }
  • debounce: Useful for search-as-you-type. Waits for a pause before emitting.
  • throttleFirst: Emits first item in a time window.
  • collectLatest: Cancels in-progress collection if new value arrives.
Cold vs Hot Flow
  • Cold: Each collector gets its own independent stream. Starts fresh on each collect call.
  • Hot: Stream runs regardless of collectors. Multiple collectors share the same emissions.

StateFlow and SharedFlow are hot. flow {} builder creates cold flows.

How to handle backpressure in Flow?
  • buffer(): Collector runs concurrently with emitter in a separate coroutine.
  • conflate(): Drop intermediate values, always process latest.
  • collectLatest {}: Cancel previous collection when new value arrives.
heavyFlow()
    .buffer(capacity = 64)
    .collect { process(it) }

Related