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.