Module 11 of 20

Concurrency, Coroutines & Flow (Deep Dive)

Master asynchronous programming, concurrency, Kotlin Coroutines, builders, dispatchers, exception handling, and cold/hot flows.

Module 11: Concurrency, Coroutines & Flow (Deep Dive)

Learning Objectives

By the end of this module, you’ll understand:

  • Why Android has a Main Thread
  • Event Loop & Message Queue
  • ANR (Application Not Responding)
  • Threads
  • Problems with raw threads
  • Kotlin Coroutines
  • Suspend functions
  • Dispatchers
  • CoroutineScope
  • Structured Concurrency
  • launch vs async
  • withContext
  • Exception handling
  • Cancellation
  • Flow
  • StateFlow
  • SharedFlow
  • Best practices

Part 1 — Why Android Has a Main Thread

Let’s start with a simple question.

When you tap a button:

User Touches Screen



Button Click



Text Changes

Who processes that touch?

The answer:

The Main Thread (also called the UI Thread).

Android has one special thread responsible for:

  • Drawing the UI
  • Processing touch events
  • Animations
  • Lifecycle callbacks
  • View updates
  • Compose recomposition scheduling

Everything visual happens here.


1. Why Only One UI Thread?

Imagine two threads changing the same TextView.

Thread A:

Text = "Hello"

Thread B:

Text = "World"

Both execute simultaneously.

Possible results:

Hello

World

Heorld

Crash

Race conditions.

To avoid this:

Android says:

Only the Main Thread may update the UI.

This greatly simplifies UI consistency.


2. The Event Loop

The Main Thread doesn’t sit idle.

Internally it continuously runs something conceptually like:

while (true) {

    Take next message

    Execute it

}

This is the event loop.

Messages include:

  • Touch events
  • Draw requests
  • Lifecycle callbacks
  • Network callback results posted back to the UI
  • Runnable objects

3. Message Queue

Imagine people waiting in line.

Queue



Touch Event



Button Click



Redraw



Animation

The Main Thread processes one message at a time.

Not two.

One.

This ordering is why the UI behaves predictably.


4. What Happens During a Button Click?

User Tap



Touch Event



Message Queue



Main Thread



Click Listener



Your Code

Notice:

Your click listener runs on the Main Thread by default.


Part 2 — The Biggest Problem

Suppose you do this:

button.setOnClickListener {

    Thread.sleep(10000)

}

For 10 seconds:

The Main Thread is sleeping.

What happens?

No Drawing

No Touch

No Animation

Frozen UI

The app appears hung.


5. ANR (Application Not Responding)

If the Main Thread remains blocked for too long (for example, around 5 seconds while processing input), Android may display:

Application Isn't Responding

The user can:

  • Wait
  • Close the app

ANRs are one of the most serious UX problems in Android.


6. Heavy Work Examples

Never perform directly on the Main Thread:

❌ Network requests

❌ Database queries

❌ Image decoding

❌ File I/O

❌ Large JSON parsing

❌ Cryptography

❌ Long-running loops

These should run on background threads.


Part 3 — Threads

The traditional solution:

Thread {

}.start()

Now the work runs separately.

Good.

But threads introduce new problems.


Problems with Threads

Imagine:

100 API calls.

Create:

100 Threads.

Each thread consumes:

  • Memory
  • Stack space
  • Scheduling overhead

Too many threads hurt performance.


Another issue:

How do you cancel them?

How do you coordinate them?

How do you wait for results?

How do you propagate errors?

Managing threads manually quickly becomes difficult.


Part 4 — Coroutines

Google recommends Kotlin Coroutines.

Important:

A coroutine is NOT a thread.

Many developers believe:

Coroutine = Thread

Incorrect.

A coroutine is a lightweight unit of work that can be suspended and resumed.

Think of it like a task that uses threads when needed.


Imagine a restaurant.

Thread:

One waiter.

Coroutine:

One customer order.

One waiter can manage multiple orders over time.

Similarly:

One thread can execute many coroutines.


Coroutine Example

viewModelScope.launch {

    val users = repository.getUsers()

}

Only one coroutine was created.

Not necessarily one thread.


Part 5 — Suspend Functions

This keyword confuses almost everyone initially.

suspend fun loadUsers()

Many think:

“suspend” means asynchronous.

Wrong.

It means:

This function may suspend its execution without blocking the underlying thread.

Imagine reading a book.

Phone rings.

You place a bookmark.

Take the call.

Return later.

Continue reading.

That’s suspension.

The thread is free to do other work while the coroutine waits.


6. How Suspension Works

Suppose:

Coroutine



API Request



Waiting...

Traditional thread:

Thread



Blocked



Idle

Coroutine:

Coroutine Suspended



Thread Released



Thread Does Other Work



API Returns



Coroutine Resumes

Much more efficient.


Part 6 — Dispatchers

Dispatchers decide where a coroutine executes.


Dispatchers.Main

Runs on:

Main Thread

Used for:

  • UI updates
  • Compose state updates
  • LiveData / StateFlow interactions

Dispatchers.IO

Optimized for:

Database

Files

Network

Large pool of threads for blocking I/O.


Dispatchers.Default

Optimized for:

CPU-intensive work.

Examples:

  • Image processing
  • Sorting
  • Compression
  • Large calculations

Dispatchers.Unconfined

Rarely needed.

Mostly for advanced scenarios and testing.

Avoid unless you understand its behavior.


7. withContext()

Suppose:

Main Thread



Need Database

Instead of creating a new coroutine:

withContext(Dispatchers.IO) {

}

Flow:

Main



IO



Database



Main

The coroutine switches context, performs the work, then resumes.

This is the preferred way to move between dispatchers.


Part 7 — Coroutine Builders


launch

Fire-and-forget.

launch {

}

Returns a Job.

Good for:

  • Updating UI state
  • Background work where no value is returned

async

Returns a result.

async {

}

Produces a Deferred<T>.

Later:

await()

to retrieve the value.


Example

Need two APIs.

Sequential:

Users



Wait



Orders

Parallel:

Users

Orders



Wait for Both

Using async can improve performance when the tasks are independent.


Part 8 — Structured Concurrency

One of the greatest strengths of coroutines.

Imagine:

Parent Coroutine



Child A



Child B



Child C

If the parent is cancelled:

All children are cancelled automatically.

This prevents “orphaned” background work.


Example:

User leaves screen.

Activity destroyed.

ViewModel cleared.

viewModelScope is cancelled.

Every child coroutine launched in that scope is cancelled.

No memory leaks.

No wasted work.


9. Coroutine Scopes

A coroutine must belong to a scope.


viewModelScope

Lives as long as the ViewModel.

Perfect for:

  • API calls
  • Database operations
  • Business logic

lifecycleScope

Lives as long as the LifecycleOwner (Activity/Fragment).

Useful for UI-related coroutines tied to lifecycle events.


GlobalScope

Avoid in application code.

Why?

It ignores lifecycle.

Coroutines may continue running long after the UI disappears.


Part 9 — Exception Handling

Suppose:

API



IOException

Without handling:

Coroutine fails.

You should use:

  • try/catch for expected failures.
  • CoroutineExceptionHandler for uncaught exceptions in root coroutines.
  • supervisorScope or SupervisorJob when you don’t want one child failure to cancel its siblings.

Part 10 — Flow

Flow represents a stream of values over time.

Imagine:

Temperature sensor.

22°



23°



24°



25°

Not one value.

Many values.

That’s Flow.


Example:

flow {

    emit(1)

    emit(2)

    emit(3)

}

The consumer receives values one by one.


Cold Flow

A Flow does nothing until collected.

Imagine Netflix.

Movie exists.

Nothing happens until you press Play.

Flow behaves similarly.

Flow



No Collector



No Execution

Every new collector starts a fresh execution.


Collecting

flow.collect {

}

Flow:

Emit



Collect



Emit



Collect

Part 11 — StateFlow

Think of StateFlow as:

A state container that always has a current value.

Example:

Loading



Success



Error

The latest state is always available.

Unlike a cold Flow, StateFlow is hot—it exists independently of collectors.


Example:

MutableStateFlow(
    UiState.Loading
)

Compose and Activities/Fragments observe it and update automatically.


Part 12 — SharedFlow

Some events should not be replayed as persistent state.

Examples:

Show Toast

Navigate

Snackbar

These are events.

Not state.

SharedFlow is suitable for broadcasting such events.

It can also be configured to replay recent emissions when appropriate.


Flow Comparison

FeatureFlowStateFlowSharedFlow
Cold/HotColdHotHot
Current valueOptional replay
State holder
Multiple collectors
Common useStreamsUI StateEvents

Part 13 — Real Android Example

User opens Home Screen.

HomeScreen



HomeViewModel



Repository



Flow<List<Product>>



StateFlow<HomeUiState>



Compose UI

When products change:

Database



Flow Emits



Repository



ViewModel



State Updated



Compose Recomposes

No manual refresh required.


Common Mistakes

❌ Launching long work on Dispatchers.Main

This blocks the UI and can lead to ANRs.


❌ Using GlobalScope

Coroutines outlive the screen and can leak work.


❌ Confusing suspend with background execution

A suspend function can still run on the Main dispatcher unless you explicitly switch context.


❌ Using StateFlow for one-time events

Navigation or Toast events may be re-delivered after configuration changes.

Use SharedFlow (or another event mechanism) instead.


❌ Collecting a Flow without respecting lifecycle

In Activities and Fragments, use lifecycle-aware collection (for example, repeatOnLifecycle) so collection stops when the UI is not visible.


Mental Model

Imagine a railway system.

Main Thread

Station (Message Queue)

Coroutines (Passengers)

Dispatchers (Tracks)

Flow (Train carrying values)
  • The Main Thread is the central station.
  • Coroutines are passengers traveling through the system.
  • Dispatchers decide which track they use.
  • Flow is a train delivering values over time.
  • Structured concurrency ensures that when the station closes (scope is cancelled), all associated journeys end safely.

Best Practices

  • Keep the Main Thread free of expensive work.
  • Use viewModelScope for ViewModel tasks.
  • Switch to Dispatchers.IO for blocking I/O.
  • Prefer withContext() over manually creating threads.
  • Model screen state with StateFlow.
  • Model one-time UI events with SharedFlow.
  • Collect Flows using lifecycle-aware APIs.
  • Let cancellation propagate naturally through structured concurrency.

Interview Questions

  1. Why does Android have a single UI thread?
  2. What causes an ANR?
  3. What’s the difference between a thread and a coroutine?
  4. What does the suspend keyword actually mean?
  5. Compare launch and async.
  6. When would you use Dispatchers.IO vs Dispatchers.Default?
  7. What is structured concurrency, and why is it important?
  8. Explain the difference between Flow, StateFlow, and SharedFlow.
  9. Why is Flow considered “cold”?
  10. Why should GlobalScope generally be avoided?

Module 11 Summary

You now understand the execution model behind modern Android apps:

  • The Main Thread handles all UI work.
  • Blocking the Main Thread leads to frozen interfaces and potentially ANRs.
  • Coroutines provide lightweight, structured concurrency without requiring one thread per task.
  • Dispatchers determine where work executes.
  • Structured concurrency automatically manages cancellation and lifecycle.
  • Flow models asynchronous streams of data.
  • StateFlow is ideal for observable UI state.
  • SharedFlow is well suited for events.

At this stage, you have the architectural and concurrency foundations used in production Android applications.


Next Module: Dependency Injection (Hilt & Dagger)

In the next module, we’ll go beyond the conceptual overview from Module 10 and dive into Dependency Injection in detail.

We’ll explore:

  • What a dependency really is.
  • Why new/constructors alone don’t scale.
  • Inversion of Control (IoC).
  • Dependency Injection patterns (constructor, field, method).
  • Why Dagger was created.
  • How Hilt builds on Dagger.
  • Code generation and dependency graphs.
  • Scopes (Singleton, ActivityRetained, ViewModel, etc.).
  • Modules, Providers, and Bindings.
  • Injecting Retrofit, Room, Repositories, and ViewModels.
  • Common DI mistakes and debugging dependency graphs.

This module is essential for understanding how large Android applications wire together dozens or hundreds of collaborating classes without becoming tightly coupled.