Stop Using launch(Dispatchers.IO) as a Thread Switch
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.
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.
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.
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.
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.
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?