Design a DispatcherProvider abstraction for injecting IO/Default/Main across a codebase, and explain how production and test wiring differ.
answer
- Interface: main/io/default as CoroutineDispatcher
- Prod = real Dispatchers; test = one shared TestDispatcher
- All test members share one scheduler
- One dependency vs many dispatcher params
- Provider.main != global Main -> still need setMain
basics
~10 sMake a small interface that exposes the dispatchers your classes need (io, default, main). Production supplies the real Dispatchers; tests supply a single test dispatcher (backed by the test scheduler) for all of them.
solid answer
~40 sDefine an interface holding each `CoroutineDispatcher` your classes use: ```kotlin interface DispatcherProvider { val main: CoroutineDispatcher val io: CoroutineDispatcher val default: CoroutineDispatcher } ``` Inject `DispatcherProvider` into classes instead of individual dispatchers, so a class with several context switches needs one dependency. Production implementation returns `Dispatchers.Main`, `Dispatchers.IO`, `Dispatchers.Default`. The test implementation returns a single scheduler-backed `TestDispatcher` for all three, so every `withContext(provider.io)` etc. runs on the shared `testScheduler` and virtual time covers the whole class. Combine with `Dispatchers.setMain` only when the code reaches `Dispatchers.Main` directly (e.g., `viewModelScope`) rather than via the provider. This keeps call sites readable, centralizes wiring (DI graph/Hilt/Koin), and avoids scattering `StandardTestDispatcher(testScheduler)` through every test.
go deeper
Can describe the idea of one interface bundling dispatchers but may miss scheduler sharing.
Writes the interface plus production/test impls and ensures all test members share one scheduler.
Explains DI-graph wiring (Hilt/Koin), the provider.main vs global Main caveat, and why one shared scheduler matters.
Weighs provider vs per-dispatcher injection vs scope injection as a codebase-wide convention and its testability/maintenance trade-offs.
## Why an abstraction Injecting raw dispatchers one-by-one works, but a class that touches IO, Default, and Main ends up with three constructor parameters. A `DispatcherProvider` bundles them behind one dependency and gives a single swap point for tests. ## The interface ```kotlin interface DispatcherProvider { val main: CoroutineDispatcher val io: CoroutineDispatcher val default: CoroutineDispatcher val unconfined: CoroutineDispatcher get() = Dispatchers.Unconfined } ``` ## Production implementation ```kotlin class DefaultDispatcherProvider : DispatcherProvider { override val main = Dispatchers.Main override val io = Dispatchers.IO override val default = Dispatchers.Default } ``` Usage: ```kotlin class Repo(private val dispatchers: DispatcherProvider) { suspend fun parse(raw: String) = withContext(dispatchers.default) { heavy(raw) } suspend fun read() = withContext(dispatchers.io) { disk() } } ``` ## Test implementation Make **all** members resolve to one scheduler-backed dispatcher so virtual time applies uniformly: ```kotlin class TestDispatcherProvider( private val dispatcher: TestDispatcher = StandardTestDispatcher(), ) : DispatcherProvider { override val main = dispatcher override val io = dispatcher override val default = dispatcher } ``` In the test, share the scheduler with `runTest`: ```kotlin @Test fun parses() { val td = StandardTestDispatcher() val repo = Repo(TestDispatcherProvider(td)) runTest(td.scheduler) { val r = repo.parse("x"); advanceUntilIdle() assertEquals(expected, r) } } ``` ## Production vs test wiring differences - **Production:** real, separate pools (`IO` for blocking, `Default` for CPU, `Main` for UI). Provided once in the DI graph (Hilt module / Koin module). - **Test:** every member is the *same* `TestDispatcher` on the *same* `TestCoroutineScheduler`, so `advanceUntilIdle()`/`advanceTimeBy()` advance everything and `delay()` is virtual. ## Main caveat A provider's `main` only helps when code uses `provider.main`. `viewModelScope` and `Dispatchers.Main.immediate` reach the global Main directly, so you still need `Dispatchers.setMain`/`resetMain` for those. ## Key APIs/terms - **`CoroutineDispatcher`**, **`TestDispatcher`**, **`TestCoroutineScheduler`**. - **`StandardTestDispatcher` / `UnconfinedTestDispatcher`** — pick based on whether you want queued or eager execution. - **DI containers (Hilt/Koin)** — provide the single `DispatcherProvider` binding.
- Should the test provider give each member a different TestDispatcher?Usually no — give them one dispatcher sharing one scheduler so a single advanceUntilIdle drives everything; separate schedulers fragment virtual time.
- Does a DispatcherProvider remove the need for setMain?Only for code that goes through provider.main. Code using viewModelScope or Dispatchers.Main directly still needs setMain/resetMain.
A DispatcherProvider is like a settings object: production reads real config, tests inject a fixture so every read is controlled.
saying these in an interview costs you the question
- Gives each provider member a distinct scheduler, breaking virtual time
- Thinks the provider replaces Dispatchers.Main globally
- Puts Dispatchers.Main.immediate logic and expects provider to intercept it
- Hardcodes the test dispatcher as the production default
- Injects a CoroutineScope when only a dispatcher abstraction is needed