Skip to content

Beginner: Kotlin · Coroutines and Flow · topic 16 of 50 · 25 min

Dispatchers and withContext

Explanation

A dispatcher decides which thread or thread pool a coroutine runs on. Dispatchers.Main is the UI thread, Dispatchers.IO is for blocking input/output like files and old blocking APIs, and Dispatchers.Default is for heavy CPU work like sorting or parsing big data. withContext(Dispatchers.IO) { } switches threads for a block and returns its result. Good suspend functions are main-safe.

Why dispatchers?

suspend lets a coroutine pause, but it does not choose a thread. Some work must be on the main thread (updating the UI), and some must stay away from it (reading a big file, compressing an image). A dispatcher is the part of the coroutine system that sends a coroutine to the right thread or pool of threads.

DispatcherRuns onUse for
Dispatchers.MainThe Android main (UI) threadUpdating UI state, calling other suspend functions
Dispatchers.IOA large shared pool of threadsBlocking I/O: files, old blocking network or database APIs
Dispatchers.DefaultA pool sized to the CPU coresCPU-heavy work: sorting big lists, parsing large JSON, image processing

viewModelScope and lifecycleScope start coroutines on Dispatchers.Main by default. That is fine, because suspend calls like Retrofit and Room switch to background threads internally.

A thread pool is a group of threads that are created once and reused for many small jobs, because creating a new thread for every job is slow. IO and Default share threads internally, but IO allows many more threads at once, since most of them are just waiting.

withContext: switch for a block

kotlin
import kotlinx.coroutines.*
import java.io.File

suspend fun readReport(path: String): String = withContext(Dispatchers.IO) {
    File(path).readText()          // blocking file read, safely off the main thread
}

suspend fun sortedNames(names: List<String>): List<String> = withContext(Dispatchers.Default) {
    names.sorted()                 // CPU work on the Default pool
}
withContext runs the block on the given dispatcher, suspends the caller until it finishes, and returns the block's value.

The main-safety rule

Google's guidance: a suspend function should be main-safe, meaning it is safe to call from the main thread. The function itself takes care of moving its blocking work to the right dispatcher with withContext. Then the callers, often ViewModels running on Main, never need to think about threads.

kotlin
class ReportRepository {
    suspend fun load(): String = withContext(Dispatchers.IO) {
        // blocking work here
        "report data"
    }
}

// In a ViewModel (runs on Main):
// viewModelScope.launch {
//     val text = repository.load()    // main-safe: no thread juggling here
//     _state.value = text
// }
Each layer is responsible for its own threading.
Think of it like this: Dispatchers are like the departments in a hospital. Main is the reception desk: it talks to patients and must never be tied up. IO is the records room, where people wait for files. Default is the lab, which does heavy analysis. withContext is walking a request to the right department and coming back with the answer.

Testing tip: inject dispatchers

Hard-coding Dispatchers.IO inside classes makes tests harder. Many teams pass the dispatcher in through the constructor, with a default value, so tests can swap in a test dispatcher (module 10).

kotlin
class ImageCompressor(private val cpu: CoroutineDispatcher = Dispatchers.Default) {
    suspend fun compress(bytes: ByteArray): ByteArray = withContext(cpu) {
        bytes.copyOf(bytes.size / 2)     // pretend compression
    }
}
Production uses Default; tests pass a test dispatcher.

Common mistakes

  • Wrapping Retrofit or Room suspend calls in withContext(Dispatchers.IO). Not harmful, but unnecessary: they are already main-safe.
  • Doing heavy CPU work on Dispatchers.IO. IO has many threads meant for waiting; CPU work belongs on Default.
  • Updating UI from a background dispatcher. Update state on Main (or through StateFlow, which is safe to collect on Main).
  • Using GlobalScope.launch(Dispatchers.IO) to "run in the background". Use a structured scope plus withContext.

Need help?

Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.

Try:

Watch the lesson

Dispatchers and withContext · 1/6

Dispatcher = which thread

suspend lets a coroutine pause. A dispatcher decides which thread it runs on.

Prefer a video by a teacher? Find free videos on “kotlin coroutines dispatchers withContext IO Default Main android” on YouTube ↗

In real life: Exporting a PDF statement

A banking app lets users export six months of transactions as a PDF. Building the PDF is CPU work, and saving it is file I/O. The repository uses withContext(Dispatchers.Default) to build it and withContext(Dispatchers.IO) to write it. The ViewModel simply calls exportStatement() from viewModelScope and shows a progress indicator meanwhile; the UI never stutters.

kotlin
import kotlinx.coroutines.*

suspend fun buildPdf(rows: Int): ByteArray = withContext(Dispatchers.Default) {
    ByteArray(rows * 100)          // pretend heavy layout work
}

suspend fun save(bytes: ByteArray): String = withContext(Dispatchers.IO) {
    "statement.pdf (${bytes.size} bytes)"   // pretend file write
}

fun main() = runBlocking {
    val pdf = buildPdf(1200)
    println("Saved " + save(pdf))   // Saved statement.pdf (120000 bytes)
}
Each step runs on the dispatcher that suits it.

Practice (0/3 done)

Task 1 · Question

Which dispatcher suits sorting a list of 2 million items?

Task 2 · Question

What does withContext(Dispatchers.IO) { readFile() } return?

Task 3 · Practice

Write a main-safe suspend fun countWords(path: String): Int that reads a text file on Dispatchers.IO and counts words on Dispatchers.Default. Print Thread.currentThread().name inside each block to see the different threads, then call it from runBlocking.

Quick quiz

Question 1

1.Which dispatcher runs on the Android UI thread?

Question 2

2.What does "main-safe" mean for a suspend function?

Question 3

3.On which dispatcher does viewModelScope.launch start by default?

Question 4

4.Do you need withContext(Dispatchers.IO) around a Retrofit suspend call?

Read more (free)

Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.

Which dispatcher runs on the Android UI thread?

Answer: Dispatchers.Main

Main is the UI thread.

What does "main-safe" mean for a suspend function?

Answer: It is safe to call from the main thread because it moves blocking work elsewhere itself

Callers do not need to switch threads.

On which dispatcher does viewModelScope.launch start by default?

Answer: Main

viewModelScope uses Dispatchers.Main (immediate).

Do you need withContext(Dispatchers.IO) around a Retrofit suspend call?

Answer: No, Retrofit suspend functions are already main-safe

The library handles threading.

Which annotation marks a JUnit test function?Related

Answer: @Test

JUnit runs every @Test function.

From Unit tests with JUnit and coroutines

What does suspend mean on a function?Related

Answer: It may pause without blocking the thread

suspend allows pausing; the dispatcher decides the thread.

From Why coroutines? suspend functions

What does launch return?Related

Answer: A Job

A Job lets you join or cancel the coroutine.

From launch, async and structured concurrency

Which is the best description of a coroutine?Related

Answer: A lightweight task that can suspend without blocking a thread

Coroutines run on threads but are much lighter.

From Interview questions: Kotlin

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

Next steps

  1. 1 more topic in “Coroutines and Flow” before its exam: Flow and StateFlow.
  2. Module 4 exam: Coroutines and Flow · 13 questions · 25 min, without help. 4 topics of this module still to finish first.
  3. Then the next module, Android Studio and your first app. 50 topics left in the course before the final exam.

← Previous: launch, async and structured concurrency