Chapter 24: Coroutines – Structured Concurrency & Flow

Hey Webliance! Welcome to Chapter 24: Coroutines – Structured Concurrency & Flow — this is where coroutines go from “cool” to absolutely essential in Kotlin! ☕🚀

In the last chapter, we covered the basics (suspend functions, runBlocking, launch, async/await). Now, we’ll dive deeper into structured concurrency (how to organize and supervise coroutines safely) and Flow (Kotlin’s reactive streams for handling asynchronous data flows — like RxJava but simpler and built-in).

We’ll cover:

  • CoroutineScope & SupervisorJob
  • withContext & Dispatchers
  • Cold & hot flows (flow { }, StateFlow, SharedFlow)
  • Operators (map, filter, collect, catch, etc.)

I’ll explain everything very slowly, like we’re sitting together in a quiet Bandra café — with real-life analogies, complete runnable examples (copy-paste into IntelliJ with coroutines dependency), step-by-step breakdowns, tables, common mistakes with fixes, and fun facts so everything sticks perfectly.

Important setup reminder:

  • Add implementation(“org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0”) to your build.gradle.kts
  • For Flow, it’s included in core — no extra dependency needed.

Let’s start!

1. CoroutineScope – The Safe Way to Group Coroutines

A CoroutineScope is a context that defines the lifecycle of a group of coroutines. It’s like a parent container that:

  • Waits for all child coroutines to finish
  • Cancels all children if one fails (structured concurrency)
  • Handles exceptions safely

Why use CoroutineScope?

  • Avoids leaks (forgotten coroutines running forever)
  • Ensures structured concurrency — children don’t outlive parent
  • Replaces runBlocking in non-blocking code

Real-life analogy: You’re a project manager (scope) with a team (coroutines).

  • You start tasks (launch/async)
  • You wait for all to finish before closing the project
  • If one team member fails, you stop the whole project

Example 1 – Basic CoroutineScope

Kotlin

Output (after 2 seconds):

text

Step-by-step:

  1. CoroutineScope(Dispatchers.Default) → creates a new scope on Default dispatcher (background threads)
  2. launch inside scope → child coroutines
  3. myScope.coroutineContext.job.join() → waits for all children (or use myScope.job.children.forEach { it.join() })

Best practice: Always cancel the scope when done (e.g., in Android ViewModel onCleared() → scope.cancel()).

2. SupervisorJob – Handle Failures Without Canceling Siblings

SupervisorJob is a special Job that:

  • Does not cancel siblings when one child fails
  • Propagates exceptions to parent

Use when: You have independent tasks — one failure shouldn’t stop others (e.g., multiple downloads).

Normal Job vs SupervisorJob:

Type If one child fails… Use for
Normal Job Cancels all siblings + propagates exception Dependent tasks (all or nothing)
SupervisorJob Other siblings continue Independent tasks (e.g., multiple API calls)

Example 2 – SupervisorJob in action

Kotlin

Output (after 1.5 seconds):

text

Step-by-step:

  1. SupervisorJob() → creates a job that supervises children independently
  2. Task 2 fails → doesn’t cancel Task 1/3
  3. Parent catches the exception from supervisor

Fun fact: In Android, viewModelScope uses SupervisorJob by default — one ViewModel task failing doesn’t kill others.

3. withContext – Switch Dispatchers Inside Coroutine

withContext is a suspend function that:

  • Switches to a different dispatcher (thread pool)
  • Suspends until the block finishes
  • Returns the result of the block

Dispatchers (thread pools):

Dispatcher Purpose Use for
Dispatchers.Default CPU-intensive tasks Heavy computation, list processing
Dispatchers.IO I/O tasks (files, network, database) API calls, file read/write
Dispatchers.Main UI updates (Android only) Update UI views
Dispatchers.Unconfined Run on current thread (rare) Testing / special cases

Example 3 – withContext to switch dispatchers

Kotlin

Step-by-step:

  1. withContext(Dispatchers.IO) { … } → moves the block to IO thread pool
  2. Suspends the coroutine → frees caller thread
  3. When done, resumes on caller dispatcher → returns result

Best practice: Use withContext for heavy I/O inside suspend functions — keeps UI/main thread free.

4. Cold & Hot Flows – Kotlin’s Reactive Streams

Flow is Kotlin’s built-in way to handle asynchronous streams of data — like RxJava’s Observable but simpler and integrated with coroutines.

Cold Flow → starts emitting data only when collected (lazy, on-demand) Hot Flow → emits data whether collected or not (shared, like live data)

A. Cold Flows (flow { })

Cold flows are created with flow { } — they are suspendable and produce values asynchronously.

Example 4 – Basic cold flow

Kotlin

Output (1 per second):

text

Step-by-step:

  1. flow { … } → creates a cold flow builder
  2. emit(value) → suspends and sends value to collector
  3. collect { … } → terminal operator — starts the flow

Cold flow property: If you call coldFlow().collect { } twice → it runs twice independently (cold start each time).

B. Hot Flows – StateFlow & SharedFlow

StateFlow → hot flow that holds a state (latest value) — like LiveData. SharedFlow → hot flow for events (no state, can replay).

StateFlow example:

Kotlin

Output:

text

SharedFlow example (events with replay):

Kotlin

Output:

text

5. Flow Operators – map, filter, collect, catch, etc.

Flow has many operators like collections — but they are suspendable and asynchronous.

Operator Purpose Example
map Transform each emission .map { it * 2 }
filter Keep emissions that match .filter { it > 0 }
collect Terminal – consume emissions .collect { println(it) }
catch Handle exceptions in upstream .catch { println(“Error: $it”) }
onEach Side effect on each emission .onEach { println(“Emitted: $it”) }
transform Transform + emit multiple .transform { emit(it); emit(it * 2) }
flowOn Change dispatcher upstream .flowOn(Dispatchers.IO)

Example 5 – Flow with operators

Kotlin

Output (every 0.5s for even numbers):

text

Step-by-step:

  1. Flow emits 1 to 10 every 0.5s.
  2. filter keeps evens.
  3. map squares them.
  4. onEach prints processed.
  5. catch handles any errors (e.g., if emit throws).
  6. collect consumes and prints final.

flowOn for dispatcher:

Kotlin

Quick Recap Table (Your Cheat Sheet)

Concept Key Points / Example Benefit
CoroutineScope CoroutineScope(Dispatchers.Default) Group & manage coroutines
SupervisorJob SupervisorJob() Independent failure handling
withContext withContext(Dispatchers.IO) { … } Switch dispatcher – non-blocking
Cold Flow flow { emit(1); emit(2) } Lazy, on-demand, cold start
StateFlow MutableStateFlow(0) Hot, holds state, shared
SharedFlow MutableSharedFlow(replay = 1) Hot, events, replay
Operators .map { … }, .filter { … }, .collect { … } Transform & consume flows

Common Newbie Mistakes & Fixes

Mistake Problem Fix
Not waiting for children Coroutines finish after main exits Use join() or coroutineScope { }
Using normal Job instead of SupervisorJob One failure cancels all Use SupervisorJob() for independence
Blocking in wrong dispatcher UI freeze (Android) or thread starvation Use Dispatchers.IO for blocking I/O
Collecting cold flow multiple times Runs multiple times (cold) Use hot flow (SharedFlow) for shared
Forgetting to import coroutines Cannot find flow, collect import kotlinx.coroutines.flow.*

Homework for You (Let’s Make It Fun!)

  1. Basic Create a scope with SupervisorJob → launch 3 coroutines → make one throw exception → see others continue.
  2. Medium Use withContext(Dispatchers.IO) in a suspend function to simulate a 2s network call.
  3. Advanced Create a cold flow that emits numbers 1 to 10 every 0.5s → use map & filter to get even squares → collect.
  4. Fun Create a StateFlow for a counter → launch coroutine that updates it every 1s → collect in main.
  5. Challenge Fix this code:
    Kotlin

You’ve just mastered structured concurrency & Flow — now you can build reactive, safe, concurrent apps like a pro!

You may also like...

Leave a Reply

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