skip to content

Ktor's `HttpClient` is a common API, but you must pick an engine per target. Explain the engine model and name appropriate engines for Android, iOS, and JS.

level: middleimportance: should knowfreq 50%

answer

  1. HttpClient = common API; engine = transport
  2. Android: OkHttp or CIO
  3. iOS/Apple: Darwin (NSURLSession)
  4. JS: Js engine (fetch)
  5. shared plugins, engine via expect/actual

basics

~20 s

Ktor gives you one common HttpClient, but the actual network calls run through a pluggable engine you choose per platform: OkHttp or CIO on Android, Darwin on iOS, Js on the web. You configure the client in shared code and supply the engine per target.

solid answer

~30 s

Ktor separates the **client API** (`HttpClient`, plugins, requests) from the **engine** that performs the transport. In `commonMain` you write portable request code; each target depends on an engine artifact and passes its `HttpClientEngineFactory` to `HttpClient(engine)`. Typical choices: **Android/JVM** — `OkHttp` (`ktor-client-okhttp`) or `CIO` (`ktor-client-cio`, coroutine-native, pure Kotlin); **iOS/Apple (Native)** — `Darwin` (`ktor-client-darwin`), wrapping `NSURLSession`; **JS** — `Js` (`ktor-client-js`), using `fetch`/`XMLHttpRequest`. A common pattern is an `expect fun httpClient(): HttpClient` whose `actual`s pick the engine, while shared config (JSON via `ContentNegotiation`, logging, timeouts) lives in a common builder. This is the same expect/actual seam: one API surface, native transport per platform.

code

kotlin · 14 lines
kotlin
// commonMain — portable request code
suspend fun fetchUser(client: HttpClient, id: String): User =
    client.get("https://api.example.com/users/$id").body()

// commonMain — engine selected per target
expect fun engine(): HttpClientEngineFactory<*>
fun api() = HttpClient(engine()) { install(ContentNegotiation) { json() } }

// androidMain
actual fun engine() = OkHttp
// iosMain
actual fun engine() = Darwin
// jsMain
actual fun engine() = Js

go deeper

for a junior

Knows you pick an engine per platform and can name OkHttp/Darwin/Js.

for a middle

Separates client API from engine, wires engine via expect/actual, knows plugins are shared.

for a senior

Adds CIO vs OkHttp tradeoffs, engine-specific config blocks, auto-selection behavior, and suspend integration.

for a principal

Designs the shared client factory and DI boundary so engine choice and per-platform networking config stay isolated from feature code.

## Client vs engine Ktor's `HttpClient` is the **common-API** surface: building requests, installing plugins (`ContentNegotiation`, `Logging`, `HttpTimeout`, `Auth`), parsing responses. It does not itself do the networking. The actual bytes-over-the-wire work is delegated to a **`HttpClientEngine`**, chosen per target. This is the platform-specific-implementation pattern: one API, many backends. ## Engines by platform - **Android / JVM** - `OkHttp` — `io.ktor:ktor-client-okhttp`. Wraps Square's OkHttp; good HTTP/2, connection pooling, interceptors. Common Android choice. - `CIO` — `io.ktor:ktor-client-cio`. Pure-Kotlin, coroutine-based engine, no extra native lib; also works on JVM and some other targets. - `Android` — `ktor-client-android` (built on `HttpURLConnection`), lighter but less featureful. - **iOS / macOS / Apple (Kotlin/Native)** - `Darwin` — `io.ktor:ktor-client-darwin`. Backed by Apple's `NSURLSession`; the idiomatic Apple engine. - **JS (browser / Node)** - `Js` — `io.ktor:ktor-client-js`. Uses the browser `fetch` API (or Node equivalents). - **JVM server-side / generic** — `CIO`, `OkHttp`, `Apache`, `Java` (HttpClient) are also available. ## How you wire it Shared configuration in common code, engine chosen per target via expect/actual: ```kotlin // commonMain expect fun httpClientEngine(): HttpClientEngineFactory<*> fun buildClient(): HttpClient = HttpClient(httpClientEngine()) { install(ContentNegotiation) { json() } install(HttpTimeout) { requestTimeoutMillis = 15_000 } } ``` ```kotlin // androidMain actual fun httpClientEngine() = OkHttp // iosMain actual fun httpClientEngine() = Darwin // jsMain actual fun httpClientEngine() = Js ``` Alternatively `HttpClient { }` with **no explicit engine** auto-selects an engine present on the classpath (Ktor service-loads one), but explicit selection is clearer and avoids surprises when multiple engines are present. ## Why it matters - **Platform fit:** `Darwin`/`NSURLSession` integrates with iOS networking (background sessions, system proxy), while `OkHttp` integrates with the Android networking stack and interceptors. - **Engine-specific config:** you can configure the engine block, e.g. `engine { config { followRedirects(true) } }` for OkHttp, or preconfigure `NSURLSession` for Darwin. - **Plugins are engine-agnostic:** `ContentNegotiation`, `Auth`, `Logging` work the same regardless of engine because they sit in the common client layer. ## Suspension model All request functions (`client.get`, `client.post`) are **`suspend`** functions returning once the response is available, integrating with coroutines on every platform; the engine adapts the platform's async I/O to that suspend contract. This is precisely the leaf's theme: **one common `HttpClient` API, platform-specific engines** picked per target.

  • If you create `HttpClient { }` with no engine argument, what happens?
    Ktor auto-selects an engine found on the classpath via service discovery. It works but is implicit; with several engines present, explicitly passing one is safer.
  • Where do plugins like ContentNegotiation and Auth run — engine or client layer?
    The client layer. Plugins are engine-agnostic, so the same install blocks work across OkHttp, Darwin, and Js.

saying these in an interview costs you the question

  • Claiming OkHttp works on iOS/Native
  • Thinking you must reconfigure plugins per engine
  • Not knowing Darwin maps to NSURLSession
  • Believing the engine choice changes the request API surface
  • Saying request functions are blocking rather than suspend

context