跪拜 Guibai
← All articles
Android

Stop Using launch(Dispatchers.IO) as a Thread Switch

By 潜龙勿用之化骨龙 ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

Mechanically writing `launch(Dispatchers.IO)` obscures responsibility boundaries and creates scheduling redundancy. Moving dispatcher decisions to the data layer makes call sites simpler and prevents the entire codebase from breaking when underlying APIs change from blocking to async.

Summary

Kotlin coroutine code is littered with `launch(Dispatchers.IO)` as a reflex for any network or database call, but this conflates three separate concerns: lifecycle, concurrency, and scheduling. `launch` exists to create concurrent child tasks, not to switch threads — that's `withContext`'s job. When the data layer already uses async APIs like Retrofit's suspend functions, wrapping calls in `Dispatchers.IO` adds a redundant scheduling layer that solves nothing.

The correct boundary is that whoever owns the execution model handles scheduling. ViewModels launch tasks; repositories guarantee Main-safe APIs by isolating blocking calls internally with `withContext(Dispatchers.IO)`. If the underlying API is already suspending, no extra dispatcher is needed. The litmus test is asking what the API's execution model actually is, not reflexively adding IO.

Takeaways
`launch` creates a concurrent child task; it is not a thread-switching API — `withContext` handles temporary context changes.
Most modern Android data-layer APIs (Retrofit, Room with suspend) already manage their own async execution and need no extra `Dispatchers.IO`.
Data layers should guarantee Main-safe suspend functions by internally wrapping blocking calls with `withContext(Dispatchers.IO)`, not by forcing callers to supply the dispatcher.
Adding `Dispatchers.IO` on top of an already-async API creates scheduling redundancy: the upper layer schedules once, the lower layer schedules again.
Before adding `Dispatchers.IO`, ask whether concurrency is needed, whether the underlying API is blocking, and who actually owns the execution model.
Conclusions

The habit of writing `launch(Dispatchers.IO)` persists because developers conflate coroutine creation with thread management — two concepts the framework deliberately separates.

Pushing dispatcher responsibility into the data layer is not just cleaner architecture; it future-proofs call sites against underlying API changes from blocking to non-blocking.

The real maturity signal in a coroutine codebase is not how many dispatchers it uses, but how few it needs at the ViewModel level.

Concepts & terms
Main-safe
A suspend function contract guaranteeing it will not block the main thread, regardless of which dispatcher the caller uses. The function internally handles any necessary dispatcher switching.
Scheduling redundancy
When both the calling layer and the underlying API layer independently apply dispatcher switches, resulting in unnecessary double-scheduling with no benefit.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗