rcosteira79/android-skills

kmp-ktor

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.

소스 보기

사용 방법

  1. 설치 명령을 복사하고 자신의 에이전트 환경에 설치하기 전에 소스 패키지를 확인하세요.
  2. 필요한 도구, 인증 정보, 지원 환경은 원본 문서에서 확인하세요.
  3. 입력과 원하는 산출물을 설명하고 출처와 결과를 대조하세요.
npx skills add https://github.com/rcosteira79/android-skills --skill kmp-ktor

시작 전 확인

  • 설치에는 모델 구독, API 한도, 파일 접근 권한이 포함되지 않습니다.
  • 별점 수와 설치 수는 활동 지표이며 독립적인 품질·보안 검증이 아닙니다.
원본 Skill 문서

원본 저장소의 제목, 예시, 코드, 표, 링크, 이미지를 유지해 표시합니다.

Ktor Client for KMP and Android

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).

Plugin install order — HttpRequestRetry BEFORE HttpTimeout

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.

kotlin
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.)

encodeDefaults = true — or protocol-constant fields silently vanish

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 — pick one model, consistently

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.

Bearer refresh — markAsRefreshTokenRequest() or it loops

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).

kotlin
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).

WebSockets & SSE — use the serialization converter

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.)

kotlin
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.

Error mapping + testing

Catch specific Ktor types at the repository (ClientRequestException / ServerResponseException / HttpRequestTimeoutException / IOException) and map to DataError — catch (e: Exception) would swallow CancellationException. The full repository pattern + DataError taxonomy lives in android-skills:android-data-layer. For richer per-error UI states (Unauthorized, RateLimited, Forbidden, …), a sealed ApiResult<T> + a safeRequest wrapper with expectSuccess = false is the alternative shape — pick one per project.

Inject HttpClientEngine so tests swap in MockEngine, reusing the production createHttpClient factory so plugin config matches:

kotlin
val mockEngine = MockEngine { request ->
    assertEquals("/users/42", request.url.encodedPath)
    respond("""{"id":"42","name":"Ada","created_at":1700000000000}""",
        HttpStatusCode.OK, headersOf(HttpHeaders.ContentType, "application/json"))
}
val repo = UserRepository(UserService(createHttpClient(mockEngine, baseUrl = "https://api.example.com/")))

관련 작업

같은 저장소의 Skills

더 많은 Skills

모든 Skills
rcosteira79
커뮤니티

android-data-layer

Use when implementing the data layer in Android or KMP — the Repository pattern's layered error-propagation model (repository as the error boundary, sealed DataError, Result placement with and without a domain layer) and Room in commonMain (@ConstructedBy, BundledSQLiteDriver, per-target KSP).

설치 수
1
GitHub Stars
155
업데이트
9월 19일
rcosteira79
커뮤니티

android-debugging

Use when debugging Android or KMP issues — Android-specific techniques covering Logcat, ADB, ANR traces, R8 stack trace decoding, memory leaks, Gradle build failures, and Compose recomposition bugs, on a root-cause-first foundation.

설치 수
1
GitHub Stars
155
업데이트
9월 19일
rcosteira79
커뮤니티

android-dev

Use this skill as the baseline for ALL Android and Kotlin Multiplatform (KMP) work — whenever the user mentions Android, Kotlin (in an Android context), KMP, CMP, commonMain, androidMain, iosMain, AndroidManifest, Gradle, build.gradle, Hilt, Dagger, Room, Retrofit, Ktor, ViewModel, LiveData, StateFlow, SharedFlow, Compose, Activity, Fragment, Intent, ADB, Logcat, MVVM, MVI, repository pattern, or any Android SDK / Jetpack / AndroidX API. Always load this skill alongside the more specific skills

설치 수
1
GitHub Stars
155
업데이트
9월 19일
rcosteira79
커뮤니티

android-ux

Use when designing or reviewing Android UI — applies Material Design 3 UX principles covering touch targets, spacing, navigation patterns, accessibility, animation timing, and platform conventions. Includes an M3 compliance audit that scores screens across 10 categories. Complements the compose skill with design-level decisions.

설치 수
1
GitHub Stars
155
업데이트
9월 19일