skip to content

How is `Dispatchers.Main` from kotlinx.coroutines backed differently on Android, iOS, and JS, and what must Android consumers add for it to work?

level: middleimportance: should knowfreq 55%

answer

  1. Main = UI-thread dispatcher, one common name
  2. Android needs kotlinx-coroutines-android artifact
  3. Missing artifact -> 'Main dispatcher is missing' ISE
  4. Main.immediate skips re-dispatch on UI thread
  5. tests: Dispatchers.setMain / resetMain

basics

~20 s

Dispatchers.Main is one common name, but each platform runs it on its own UI loop: Android's main Handler, iOS's main run loop, JS's event loop. On Android you must add the kotlinx-coroutines-android artifact so the real Main dispatcher is present.

solid answer

~40 s

`Dispatchers.Main` is a common-API `CoroutineDispatcher` that schedules work on the platform's UI thread. Its implementation is platform-specific (the classic expect/actual pattern): on Android/JVM it dispatches via a `Handler` on the main `Looper`, supplied by the `kotlinx-coroutines-android` dependency through a `MainDispatcherFactory` (service-loaded). Without that artifact, touching `Dispatchers.Main` throws an `IllegalStateException` saying the Main dispatcher is missing. On iOS (Kotlin/Native) it posts to the main run loop. On JS it uses the browser/Node microtask-and-task scheduling. There is also `Dispatchers.Main.immediate`, which skips re-dispatch when you are already on the main thread, avoiding an unnecessary post. Common code just references `Dispatchers.Main`; the correct backend is wired per target at build/runtime.

code

kotlin · 12 lines
kotlin
// Common code, portable across targets
fun load(scope: CoroutineScope) = scope.launch(Dispatchers.Main) {
    val data = withContext(Dispatchers.Default) { compute() }
    showOnUi(data) // safe: we're back on the UI thread
}

// Android: build.gradle.kts
// implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:<ver>")

// Unit test (JVM)
@BeforeTest fun setup() = Dispatchers.setMain(StandardTestDispatcher())
@AfterTest fun tearDown() = Dispatchers.resetMain()

go deeper

for a junior

Knows Main is the UI dispatcher and that Android needs an extra artifact.

for a middle

Explains per-platform backing, the missing-dispatcher error, and Main.immediate.

for a senior

Adds MainDispatcherFactory/ServiceLoader wiring, setMain/resetMain for tests, and how Default/IO differ across targets.

for a principal

Reasons about dispatcher injection vs hardcoding Main for testability and platform portability in a shared library's public API.

## What `Dispatchers.Main` is A `CoroutineDispatcher` determines which thread(s) a coroutine resumes on. `Dispatchers.Main` is the **UI-thread dispatcher**: launching with it (`launch(Dispatchers.Main) { ... }`) runs the body on the platform's main/UI thread so you can touch UI safely. It is declared as a **common API** in kotlinx.coroutines, but its concrete behavior is **platform-specific**. ## Per-platform backing - **Android / JVM** — backed by a `MainCoroutineDispatcher` that posts `Runnable`s to a `Handler` bound to the main `Looper`. This implementation lives in the **`org.jetbrains.kotlinx:kotlinx-coroutines-android`** artifact and is discovered via a `MainDispatcherFactory` registered through the JVM `ServiceLoader` mechanism. - **iOS / Kotlin/Native (Apple)** — dispatches onto the **main run loop / main dispatch queue** so callbacks land on the UI thread. - **JS** — uses the JavaScript **event loop** (microtask + macrotask scheduling) since JS is single-threaded. - **`Dispatchers.Main.immediate`** — a variant that executes the block **without re-dispatching** if the caller is already on the main thread, otherwise posts as usual. Useful to avoid an extra event-loop hop (e.g. updating UI synchronously when already on the UI thread). ## The Android gotcha The base `kotlinx-coroutines-core` does **not** include the Android Main dispatcher. If you reference `Dispatchers.Main` without `kotlinx-coroutines-android` on the classpath you get: ``` IllegalStateException: Module with the Main dispatcher is missing. Add dependency providing the Main dispatcher, e.g. 'kotlinx-coroutines-android' ``` So for Android apps you add: ```kotlin dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:<version>") } ``` For tests, **`kotlinx-coroutines-test`** provides a way to install a test dispatcher as Main via `Dispatchers.setMain(testDispatcher)` / `Dispatchers.resetMain()`, so Main works in unit tests off-device. ## Why this is the expect/actual story Common shared code can write: ```kotlin fun observe(scope: CoroutineScope) { scope.launch(Dispatchers.Main) { render(loadData()) } } ``` and it compiles for every target. Each target supplies the real Main dispatcher: Android via the android artifact, Apple/JS via their built-in actuals in the multiplatform coroutines library. The **call site is portable**; the **scheduling primitive is native** per platform — exactly the pattern the leaf describes. ## Related dispatchers - `Dispatchers.Default` — shared background pool for CPU work (common across platforms, sized to CPU count on JVM/Native; on JS it maps to the event loop). - `Dispatchers.IO` — JVM/Android only, for blocking IO; not available on Native/JS in the same form. - `Dispatchers.Unconfined` — runs in the current thread until first suspension.

  • What is the difference between `Dispatchers.Main` and `Dispatchers.Main.immediate`?
    `immediate` runs the block synchronously if you are already on the main thread, avoiding a re-dispatch; plain `Main` always posts to the queue first.
  • How do you make `Dispatchers.Main` usable in JVM unit tests with no UI loop?
    Use `kotlinx-coroutines-test`: call `Dispatchers.setMain(testDispatcher)` before and `Dispatchers.resetMain()` after, replacing Main with a controllable test dispatcher.

saying these in an interview costs you the question

  • Thinking Dispatchers.Main is in coroutines-core for Android without the android artifact
  • Claiming Dispatchers.IO exists identically on iOS/Native and JS
  • Saying Main and Main.immediate behave the same
  • Not knowing the 'Main dispatcher is missing' error or its cause
  • Believing JS has real multithreaded dispatchers

context