跪拜 Guibai
← Back to the summary

The Seven Levels of Kotlin Coroutines: From launch(IO) to Execution Model Design

Many Android developers follow almost the same learning path with coroutines.

From the very beginning:

Just make it run.

To the end:

Start designing asynchronous boundaries, scheduling strategies, and data flow models.

This is a very clear cognitive upgrade process.


Level 1: launch(IO) Everywhere

When first encountering coroutines, most people's code looks like this:

button.setOnClickListener {
    CoroutineScope(Dispatchers.IO).launch {
        val user = api.getUser()

        withContext(Dispatchers.Main) {
            tv.text = user.name
        }
    }
}

Or:

GlobalScope.launch(Dispatchers.IO) {
}

The project is full of:

launch(IO)
launch(IO)
launch(IO)
launch(IO)

Characteristics:

The result:

Main
 ↓
IO
 ↓
IO
 ↓
IO
 ↓
Main

There are more thread switches than business logic.

The core understanding at this stage:

Coroutine = creating a thread.

In reality:

A coroutine is never just a thread management framework.


Level 2: withContext() Everywhere

suspend fun loadUser(): User {
    return withContext(Dispatchers.IO) {
        api.getUser()
    }
}

Then:

suspend fun queryUser(): User {
    return withContext(Dispatchers.IO) {
        dao.query()
    }
}

And then:

suspend fun updateUser() {
    withContext(Dispatchers.IO) {
        dao.insert()
    }
}

Finally the project becomes:

withContext(IO)
withContext(IO)
withContext(IO)
withContext(IO)

Even:

withContext(IO) {
    repository.load()
}

repository:

withContext(IO) {
    remote.load()
}

remote:

withContext(IO) {
    retrofit.load()
}

Thread path:

Main
 ↓
IO-1
 ↓
IO-2
 ↓
IO-3

Although it's a bit better than launch(IO),

the essence is still:

Using coroutines as a thread-switching framework.


Level 3: Starting to Define suspend

At this stage, one begins to realize:

Asynchronous capability should sink downward.

Start writing:

interface UserRepository {
    suspend fun getUser(): User
}

Instead of:

fun getUser(callback: (User) -> Unit)

Or:

fun getUser(): Deferred<User>

ViewModel:

viewModelScope.launch {
    val user = repository.getUser()
}

Repository:

override suspend fun getUser(): User {
    return api.getUser()
}

At this point, one begins to understand:

suspend describes asynchronous capability.

And also begins to understand:

Synchronous writing style
Asynchronous execution

This is how coroutines express asynchrony.


Level 4: Starting to Understand Main Safety

Digging deeper reveals that the one truly responsible for threading should be:

Data Layer

Not:

ViewModel

For example:

override suspend fun getUser(): User {
    return  dao.query()
    
}

So the upper layer becomes:

viewModelScope.launch {
    val user = repository.getUser()
}

The ViewModel doesn't need to know whether these are blocking:

This is:

Main Safe

The caller doesn't need to care about threads.

This has already entered the architectural design phase.


Level 5: Starting to Understand "Not All suspend Needs IO"

Many people eventually discover that the following is completely unnecessary:

withContext(IO) {
    retrofitApi.load()
}

Because:

Retrofit suspend
OkHttp Dispatcher

are already asynchronous themselves.


Room:

Room Query Executor

DataStore:

DataStore Scope

Coil:

ImageLoader Dispatcher

Many SDKs:

Own thread pool

Wrapping another layer of withContext(IO) at this point often just creates:

Main
 ↓
IO Dispatcher
 ↓
SDK Thread Pool

An extra dispatch relay station.

This is also why many people see a crazy growth in thread count:

DefaultDispatcher-worker-1
DefaultDispatcher-worker-2
OkHttp Dispatcher
Room Executor

Level 6: Starting to Design Execution Models, Not Dispatchers

Truly advanced coroutine developers rarely discuss:

Should I use IO here or not?

Instead, they are thinking:

Who owns the execution right?

For example:

Application:

private val appExecutor =
    Executors.newFixedThreadPool(4)

val applicationScope =
    CoroutineScope(
        SupervisorJob() +
        appExecutor.asCoroutineDispatcher()
    )

All business tasks:

applicationScope.launch {
}

Truly blocking work:

withContext(ioDispatcher) {
}

The entire App:

This has begun to enter:

Execution model design

Instead of:

Thread switching tricks

Level 7: Forget Dispatchers, Start Thinking About Boundaries

Finally, one discovers that only three questions truly matter for coroutines:

Who is responsible for the lifecycle?

viewModelScope
applicationScope
lifecycleScope

Who is responsible for cancellation?

Job
SupervisorJob

Who is responsible for execution?

Retrofit
Room
DataStore
AppExecutor

And:

Dispatchers.IO
Dispatchers.Default

appear less and less frequently.


The Final Realm

From:

Coroutine = Creating a thread

To:

Coroutine = Asynchronous programming

Then to:

Coroutine = Lifecycle management

And finally to:

Coroutine = Execution model design

This is basically the cognitive upgrade path for an Android developer going from junior to senior, and then to architect, regarding coroutines.

Many people study coroutines for years but still remain stuck on:

Should this interface switch to IO?

While the real question has long since become:

Who should execute this task?
Who should own the scheduling right?
Who should own the lifecycle?

When one starts asking these questions, it means they have begun to truly understand coroutines.

References

From Callback to Coroutines: The Evolution of Android Asynchronous Concurrency Solutions

The word "Structured" essentially means—turning chaotic things into organized, rule-based, bounded things

From "Caller Walking on Thin Ice" to "Interface's Natural Semantics": Insights from Room/DataStore/Retrofit

Why Modern Android Officially Recommends Repository Expose suspend fun Instead of launch Internally

# Android Architecture Guide: Data Layer Should Stop Exposing start/stop: Use Flow to Take Over Lifecycle

Stop launch(IO): 3 Hidden Anti-patterns of Coroutine Thread Switching

Understanding Android Clean Architecture from Food Delivery: Why the Boss Doesn't Need to Know What Car the Delivery Guy Drives

Getting Started with Android Clean Architecture with a Small Demo

Modern Android Architecture Doesn't Need an Event Bus

Why I Don't Handle Exceptions Directly in Android ViewModel?