跪拜 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.
From the discussion

The discussion confirms the core argument: Retrofit's suspend functions already handle threading, making an extra IO dispatcher redundant. The real risk surfaces around custom suspend functions that hide blocking calls, raising the question of whether Main-safe should be an explicit contract in repository interfaces.

Retrofit's built-in suspend support eliminates the need for an explicit Dispatchers.IO switch.
Achieving Main-Safe in the data layer means callers only need to manage coroutine scope, not execution threads.
Treating any suspend function as non-blocking is a common pitfall when the function internally runs blocking code.
Whether the Main-safe guarantee should be an explicit part of the repository's interface contract is an open question.
Featured comments
無表情 1 likes

Written with some substance, meaning the app fetches data from the backend without adding IO.

潜龙勿用之化骨龙

If it's Retrofit's suspend, then it's not needed.

echoVic

The easiest pitfall here is treating suspend as non-blocking. The Retrofit example is fine, but a self-written suspend fun might still directly run blocking calls. Do you explicitly write Main-safe in the Repository's interface contract?

See top comments, translated →
Source: juejin.cn ↗ Google Translate ↗ Backup ↗