Small Kotlin Multiplatform helpers that make Ktor client calls easier to return, inspect, and handle.
sourceSets {
val commonMain by getting {
dependencies {
implementation("io.github.androidpoet:ktor-boost:$version")
}
}
}For typed WebSocket and SSE helpers, add the optional realtime module:
implementation("io.github.androidpoet:ktor-realtime:$version")Or pick protocol-specific modules:
implementation("io.github.androidpoet:ktor-realtime-websocket:$version")
implementation("io.github.androidpoet:ktor-realtime-sse:$version")
implementation("io.github.androidpoet:ktor-realtime-reverb:$version")
implementation("io.github.androidpoet:ktor-realtime-socketio:$version")
implementation("io.github.androidpoet:ktor-realtime-stomp:$version")
implementation("io.github.androidpoet:ktor-realtime-graphql:$version")
implementation("io.github.androidpoet:ktor-realtime-mqtt:$version")
implementation("io.github.androidpoet:ktor-realtime-rsocket:$version")
implementation("io.github.androidpoet:ktor-realtime-longpolling:$version")- Simple
Result<T>wrappers for Ktor HTTP calls. - Typed
NetworkResult<T, E>for status codes, headers, raw error bodies, and decoded API errors. - Bearer token helpers with automatic refresh and request replay on
401. - Retry and timeout helpers for transient network failures.
- KMP-safe byte downloads with progress callbacks.
- Empty response support with
Unit. - Async helpers returning
Deferred<Result<T>>. - Request builder shortcuts for bearer auth, query params, JSON bodies, and form bodies.
- Suspend-friendly
Resulthelpers. - Boost operators for typed HTTP errors, recovery helpers, and app-friendly failure messages.
- Optional
ktor-realtimemodule for typed WebSocket and SSE event flows. - Unified realtime endpoint model for WebSocket, SSE, Reverb, Socket.IO, STOMP, GraphQL subscriptions, MQTT-over-WS, RSocket, and long-polling adapters.
Use getResult, postResult, putResult, deleteResult, patchResult, headResult, or
optionsResult when you want a simple Kotlin Result<T>.
val result = httpClient.getResult<List<Movie>>("trendingMovies")
result
.onSuccess { movies ->// render movies
}
.onFailure { error ->// show error
}If you want non-2xx HTTP responses to become Result.failure(...), configure Ktor with
expectSuccess = true:
val httpClient =HttpClient {
expectSuccess =true
}| Use case | API |
|---|---|
| Simple success/failure handling | getResult<T>() |
| Need status code, headers, or error body | getNetworkResult<T, E>() |
| Authenticated request with token refresh | getAuthenticatedNetworkResult<T, E>() |
| Retry transient failures | getResultWithRetry<T>() |
| Download bytes with progress | downloadBytes() |
Empty response body, such as 204 No Content | deleteResult<Unit>() |
Need a Deferred<Result<T>> | getResultAsync<T>() |
| Add common headers, query params, or body | bearerToken, queryParams, jsonBody, formBody |
Need suspend callbacks on Result | onSuccessSuspend, onFailureSuspend, foldSuspend |
Use NetworkResult when your app needs response metadata or typed API errors.
Detailed operator docs: docs/network-result-operators.md.
val result = httpClient.getNetworkResult<User, ApiError>(
urlString ="users/me",
decodeErrorBody = { rawBody ->
json.decodeFromString<ApiError>(rawBody)
},
)
when (result) {
isNetworkResult.Success-> {
val user = result.body
val statusCode = result.statusCode
val headers = result.headers
}
isNetworkResult.HttpError-> {
val statusCode = result.statusCode
val rawErrorBody = result.rawBody
val apiError = result.errorBody
}
isNetworkResult.ResponseDecodingError-> {
val cause = result.cause
}
isNetworkResult.RequestError-> {
val cause = result.cause
}
}NetworkResult works with or without Ktor's expectSuccess setting.
Use Boost operators when you want a more expressive app-facing API:
httpClient.getResult<User, ApiError>("users/me", json)
.onSuccess { result ->
render(result.body)
}
.onUnauthorized {
session.refreshToken()
}
.onRateLimited { rateLimit, _ ->
scheduleRetry(rateLimit.retryAfterSeconds)
}
.recoverRequestError {
cache.user()
}
.onError { result ->
showMessage(result.messageOrNull ?:"Something went wrong")
}For realtime chat or presence streams, add ktor-realtime.
Detailed chat module docs: docs/chat-module.md.
httpClient.realtimeChat<ChatEvent, ChatCommand>(
urlString ="wss://example.com/chat",
onMessage = { event ->
render(event)
},
) {
sendJson(ChatCommand.Join(roomId ="general"))
}Protocol-neutral entrypoint:
val endpoint =RealtimeEndpoint.WebSocket("wss://example.com/realtime")
httpClient.realtime<ChatEvent, ChatCommand>(
endpoint = endpoint,
onEvent = { event -> render(event) },
)WebSocket, ServerSentEvents, Reverb, Socket.IO, STOMP, GraphQL subscriptions, MQTT-over-WS, RSocket, and long-polling are implemented with protocol-specific entrypoints.
Protocol-specific entrypoints are split as dedicated APIs (realtimeReverb, realtimeSocketIo, realtimeStomp, realtimeGraphQlSubscriptions, realtimeMqttOverWebSocket, realtimeRSocket, and realtimeLongPolling).
End-to-end realtime tests are available for WebSocket, SSE, and long-polling.
./gradlew realtimeIntegrationTestThis task:
- starts Docker Compose test infrastructure from
scripts/realtime-integration/docker-compose.yml - runs
:ktor-realtime:desktopIntegrationTest - tears down the containers
If Docker is not installed/running, use local unit tests instead:
./gradlew :ktor-realtime:desktopTestYou can also use convenience helpers:
val user = result.getOrNull()
val apiError = result.errorOrNull()
val statusCode = result.statusCodeOrNull()
val displayName =
result
.map { user -> user.name }
.getOrNull()Use BearerTokenProvider when authenticated APIs need automatic token refresh. Your app owns
token storage; KtorBoost asks for the current token, refreshes when needed, and replays the
request after a 401.
classAppTokenProvider(
privatevaltokenStore:TokenStore,
privatevalauthApi:AuthApi,
) : BearerTokenProvider {
overridesuspendfuncurrentToken(): String? {
return tokenStore.accessToken
}
overridesuspendfunrefreshToken(): String? {
val token = authApi.refreshAccessToken(tokenStore.refreshToken)
tokenStore.accessToken = token
return token
}
overridesuspendfunclearToken() {
tokenStore.clear()
}
}Then call authenticated helpers:
val result = httpClient.getAuthenticatedNetworkResult<User, ApiError>(
urlString ="users/me",
tokenProvider = appTokenProvider,
decodeErrorBody = { rawBody ->
json.decodeFromString<ApiError>(rawBody)
},
)For simple Result<T>:
val result = httpClient.getAuthenticatedResult<User>(
urlString ="users/me",
tokenProvider = appTokenProvider,
)Behavior:
- If
currentToken()returns a token, KtorBoost sendsAuthorization: Bearer <token>. - If
currentToken()returnsnull, KtorBoost callsrefreshToken()before the first request. - If the server returns
401, KtorBoost callsrefreshToken()and replays the request once. - If the replay still returns
401, KtorBoost callsclearToken()and returns the error. - KtorBoost does not cache tokens internally; your
BearerTokenProviderremains the source of truth.
Use retry helpers for transient failures such as 408, 429, 500, 502, 503, and 504.
val result = httpClient.getResultWithRetry<User>(
urlString ="users/me",
retryPolicy =RetryPolicy(maxRetries =3),
timeout =5.seconds,
)For typed errors:
val result = httpClient.getNetworkResultWithRetry<User, ApiError>(
urlString ="users/me",
retryPolicy =RetryPolicy(maxRetries =3),
timeout =5.seconds,
decodeErrorBody = { rawBody ->
json.decodeFromString<ApiError>(rawBody)
},
)Use downloadBytes for KMP-safe downloads with progress. The core API returns bytes and
metadata; apps can decide where to store the bytes on each platform.
val result = httpClient.downloadBytes(
urlString ="files/report.pdf",
onProgress = { progress ->val fraction = progress.fraction
val bytesRead = progress.bytesRead
val totalBytes = progress.totalBytes
},
)
when (result) {
isDownloadResult.Success-> {
val bytes = result.content.bytes
val contentType = result.content.contentType
val contentLength = result.content.contentLength
}
isDownloadResult.HttpError-> {
val statusCode = result.statusCode
val rawErrorBody = result.rawBody
}
isDownloadResult.RequestError-> {
val cause = result.cause
}
}DownloadResult.Success includes:
bytes: downloadedByteArray.statusCode: HTTP status code.headers: response headers.contentLength: value fromContent-Length, when available.contentType: parsed response content type, when available.
DownloadProgress includes:
bytesRead: bytes received so far.totalBytes: total size when the server sendsContent-Length.fraction: progress from0.0to1.0when total size is known.
Use small request builder helpers to keep call sites readable.
val result = httpClient.postResult<User>("users") {
bearerToken(token)
queryParams(mapOf("source" to "android"))
jsonBody(CreateUserRequest(name ="Ranbir"))
}For endpoints that return no body, request Unit.
val result = httpClient.deleteResult<Unit>("users/123")Async helpers return Deferred<Result<T>>.
val deferredResult = httpClient.getResultAsync<List<Movie>>("trendingMovies")
val result = deferredResult.await()KtorBoost also includes suspend-friendly Result helpers:
result
.onSuccessSuspend { movies ->
repository.save(movies)
}
.onFailureSuspend { error ->
logger.log(error)
}val message =
result.foldSuspend(
onSuccess = { movies ->"Loaded ${movies.size} movies" },
onFailure = { error -> error.message ?:"Something went wrong" },
)Existing simple helpers are still available:
getResultpostResultputResultdeleteResultpatchResultheadResultoptionsResultgetResultAsyncpostResultAsyncputResultAsyncdeleteResultAsyncpatchResultAsyncheadResultAsyncoptionsResultAsync
Recommended release version: 1.1.0.
This release adds NetworkResult, auth refresh helpers, retry helpers, request builder shortcuts,
downloads, and fixes coroutine behavior:
runCatchingSuspendnow rethrowsCancellationException.- Async helpers now return a real pending
Deferred<Result<T>>.
Contributions are welcome! If you've found a bug, have an idea for an improvement, or want to contribute new features, please open an issue or submit a pull request.
Support it by joining stargazers for this repository. β
Also, follow me on GitHub for my next creations! π€©
Copyright 2023 AndroidPoet (Ranbir Singh)
Licensed under the Apache License, Version 2.0.
See LICENSE.txt for details.
