android-data-layer
Use when implementing the data layer in Android or KMP — the Repository pattern's layered error-propagation model (repository as…
Use when setting up or working with Ktor client in KMP or Android projects — HttpClient configuration, per-platform engine selection, kotlinx.serialization, bearer auth with refresh, MockEngine testing, and error mapping at the repository boundary.
$ npx -y skills add rcosteira79/android-skills --skill kmp-ktor --agent claude-codeHow it fires
How this skill gets triggered: by you, by Claude, or both.
/kmp-ktorContext preview
The summary Claude sees to decide when to auto-load this skill.
Use when setting up or working with Ktor client in KMP or Android projects — HttpClient configuration, per-platform engine selection, kotlinx.serialization, bearer auth with refresh, MockEngine testing, and error mapping at the repository boundary.
name: kmp-ktor description: Use when setting up or working with Ktor client in KMP or Android projects — HttpClient configuration, per-platform engine selection, kotlinx.serialization, bearer auth with refresh, MockEngine testing, and error mapping at the repository boundary.
This reference covers the Ktor client configuration traps — plugin install order, serialization flags, auth refresh, and error mapping — not the basics of a shared `HttpClient`, engine selection, or `ContentNegotiation` setup. **Related:** `android-skills:android-data-layer` (repository + the `DataError` error model — its canonical home), `android-skills:android-retrofit` (Android-only equivalent).
The install order most often gotten wrong: installing `HttpTimeout` before `HttpRequestRetry`. Plugins run in install order for outgoing requests; retries must be able to catch timeout errors, so retry has to wrap timeout.
val json = Json { ignoreUnknownKeys = true; coerceInputValues = true; encodeDefaults = true } // encodeDefaults: see the section below
HttpClient(engine) {
install(ContentNegotiation) { json(json) }
install(Auth) { bearer { /* loadTokens / refreshTokens */ } }
install(HttpRequestRetry) { // BEFORE HttpTimeout
retryOnServerErrors(maxRetries = 3)
exponentialDelay()
}
install(HttpTimeout) { // AFTER HttpRequestRetry
requestTimeoutMillis = 30_000; connectTimeoutMillis = 15_000; socketTimeoutMillis = 15_000
}
}Reversed, `HttpTimeout` resolves the request as failed before the retry plugin sees it, so timeouts are never retried. (Separately, the `Auth` plugin handles 401 refresh independently — let `HttpRequestRetry` cover transient/5xx failures; don't chain the two around the same status code.)
`kotlinx.serialization` defaults to `encodeDefaults = false`, which **strips any property whose value equals its declared default** from the serialized output. A `jsonrpc: String = "2.0"` (or `version = "1.0"`, `type = "..."`) disappears from the payload; the server rejects every request with a generic "invalid request," and the fix is a one-line flag — found only after hours chasing HTTP-layer red herrings. Always set it for client APIs — the `val json` defined at the top of this file does, alongside `ignoreUnknownKeys` and `coerceInputValues`. That one configured instance is what the whole client shares: `install(ContentNegotiation) { json(json) }` and the WebSocket converter both take it.
`expectSuccess = true` makes Ktor throw `ClientRequestException` (4xx) / `ServerResponseException` (5xx) on non-2xx — and that throw **runs before any manual status check**, so an `if (response.status == OK)` branch after it is dead code. Pick one model project-wide: `expectSuccess = true` + `try/catch` (matches the repository pattern), or `expectSuccess = false` + explicit `response.status.isSuccess()` inspection. Never mix them.
In the `Auth` `bearer { refreshTokens { … } }` block, mark the refresh POST with `markAsRefreshTokenRequest()` so it isn't intercepted by the same `Auth` plugin — without it, a failing refresh triggers another refresh, looping infinitely. It's an `HttpRequestBuilder` extension: call it **inside the request builder block**, not bare in `refreshTokens { }` (where it doesn't compile).
install(Auth) {
bearer {
loadTokens { tokenStorage.getTokens()?.let { BearerTokens(it.access, it.refresh) } }
refreshTokens {
val refresh = oldTokens?.refreshToken ?: return@refreshTokens null
val r = client.post("auth/refresh") {
markAsRefreshTokenRequest() // skip the Auth plugin for this call
setBody(RefreshRequestDto(refresh))
}.body<TokenResponseDto>()
tokenStorage.save(r.accessToken, r.refreshToken); BearerTokens(r.accessToken, r.refreshToken)
}
sendWithoutRequest { it.url.pathSegments.none { seg -> seg in listOf("login", "register") } }
}
}Keep `BearerTokens` at the plugin boundary; the rest of the app uses your own token type. `TokenStorage` is project-defined (DataStore on Android/JVM, Keychain on iOS).
For real-time transports, install the kotlinx-serialization converter so typed messages flow over the same `Json` config as `ContentNegotiation`; without it you hand-encode/decode `Frame.Text`. (SSE = server→client only, plain HTTP, built-in reconnect; WebSocket = bidirectional, manual reconnect, binary frames — default to SSE when the client only consumes.)
val client = HttpClient(engine) {
install(WebSockets) {
pingIntervalMillis = 30_000
contentConverter = KotlinxWebsocketSerializationConverter(json) // the shared configured instance — bare `Json` reverts to encodeDefaults = false
}
install(SSE)
}
client.webSocket("wss://api.example.com/ws") {
sendSerialized(SubscribeMessage(topic = "items"))
while (true) { val msg = receiveDeserialized<ServerMessage>(); /* handle */ }
}
// SSE — incoming is a Flow<ServerSentEvent>
client.sse("https://api.example.com/events") { incoming.collect { event -> /* event.event / event.data / event.id */ } }Wrap SSE/WebSocket collection in a `LaunchedEffect` or repository coroutine so cancellation closes the HTTP connection when the consumer goes away.
Catch **specific** Ktor types at the repository (`ClientRequestException` / `ServerResponseException` / `HttpRequestTimeoutException` / `IOException`) and map to `DataError` — `catch (e: Exception)` would swallow `CancellationException`. The full rep
Android & KMP development skills for Claude Code, Codex, and Copilot CLI — architecture, Compose, coroutines, flows, networking, persistence, dependency injection, testing, debugging, and Gradle.
Use when implementing the data layer in Android or KMP — the Repository pattern's layered error-propagation model (repository as…
Use when debugging Android or KMP issues — Android-specific techniques covering Logcat, ADB, ANR traces, R8 stack trace decoding,…
Use this skill as the baseline for ALL Android and Kotlin Multiplatform (KMP) work — whenever the user mentions Android, Kotlin…
Use when setting up or refactoring Android Gradle build logic — convention plugins, composite builds, version catalogs, and…
Use when setting up or working with Retrofit in Android — service interface definitions, coroutines integration, OkHttp…
Use when needing to fetch Android source code — AOSP platform internals (@hide APIs, framework classes, system services) or…