Skip to main content

Dispatchers and coroutine context

A coroutine context carries elements such as its Job, dispatcher, name, and exception handler.

Dispatchers​

DispatcherTypical use
MainUI and main-thread-confined APIs
DefaultCPU-heavy calculations
IOBlocking file, database driver, or legacy network APIs
UnconfinedSpecialized cases/tests; not a general background dispatcher

Well-designed suspending functions are main-safe: callers may invoke them from Main because the function moves its own blocking work.

suspend fun readConfig(): Config = withContext(Dispatchers.IO) {
parse(file.readText())
}

withContext suspends until the block finishes and then resumes in the caller's context.

✕ Caller must guess implementation needs

viewModelScope.launch(Dispatchers.IO) {
val user = repository.user() // later updates UI from wrong context
}

✓ Repository makes blocking implementation main-safe

viewModelScope.launch {
val user = repository.user()
_uiState.value = user.toUi()
}

Testing​

Inject dispatchers or a dispatcher provider so tests use StandardTestDispatcher and virtual time. Avoid hard-coding Dispatchers.IO deep inside code that must be deterministic in unit tests unless the boundary is intentionally hidden behind a fake.

Interview-ready answer

The dispatcher decides where coroutine code executes; suspend alone does not. Main is for UI, Default for CPU work, and IO for blocking I/O. Suspending APIs should usually be main-safe and own their dispatcher switching.