A lightweight HTTP client for Kotlin Multiplatform based on Ktor. It provides a simple builder-style API, typed configuration, interceptors (logging, authentication, error handling), and a unified response model. Compatible with Android and iOS.
- Core API:
shared/src/commonMain/kotlin/com/santimattius/http/ - Entry point:
HttpClient(default singleton and custom clients) - Request builder:
HttpRequest - Response:
HttpResponse - Configuration:
HttpClientConfig - Interceptors:
com.santimattius.http.interceptor.*
Brief description:
KMP HTTP Client simplifies the use of HTTP in Android, iOS, and Kotlin Multiplatform projects. It wraps Ktor with a small, consistent API, reducing boilerplate and centralizing configuration and observability.
Problem it solves:
It unifies HTTP usage across Android and iOS while keeping a minimal surface. It avoids duplicating Ktor setup per platform, standardizes request building, and enables handling cross-cutting concerns.
Prerequisites:
- Kotlin Multiplatform properly configured.
- Android: add
android.permission.INTERNETinAndroidManifest.xml; respectminSdkandcompileSdkdefined inshared/build.gradle.kts. - iOS: Xcode (iOS 14+ recommended) and integration of the generated
Sharedframework. - Ktor and Kotlinx Serialization are already included in the
sharedmodule.
repositories {
mavenCentral()
}
dependencies {
// Replace with actual coordinates when publishing
implementation("io.github.santimattius.kmp:http-client:<version>")
}Notes:
- Android engine: Ktor OkHttp (
ktor-client-okhttp). - Initialize once (e.g., in
Application#onCreateor using AndroidX Startup).
The generated framework is called KMPHttpClient (defined in shared/build.gradle.kts with baseName = "KMPHttpClient"). Integration options:
- Xcode + KMP plugin: include
sharedand let Gradle generateKMPHttpClient.framework. - Prebuilt XCFramework: create
KMPHttpClient.xcframeworkand add it to your iOS project. - Swift Package Manager wrapper: define a
Package.swiftpointing to the publishedKMPHttpClient.xcframework(binary target).
Example with SwiftPM (binary target):
// Package.swift (example)
import PackageDescription
let package = Package(
name: "KMPHttpClient",
platforms: [ .iOS(.v14) ],
products: [ .library(name: "KMPHttpClient", targets: ["KMPHttpClient"]) ],
targets: [
.binaryTarget(
name: "KMPHttpClient",
url: "https://github.com/your-org/kmp-http-client/releases/download/1.0.0/KMPHttpClient.xcframework.zip",
checksum: "<swiftpm-checksum>"
)
]
)In Swift:
import KMPHttpClientSkie is enabled to improve Swift interoperability:
skie {
swiftBundling {
enabled = true
}
}Initialize the client once and reuse the default instance.
Kotlin (Android):
import com.santimattius.http.HttpClient
import com.santimattius.http.HttpRequest
import com.santimattius.http.config.HttpClientConfig
// Example in Application.onCreate
HttpClient.initialize(
HttpClientConfig(baseUrl = "https://api.example.com")
.connectTimeout(30_000)
.socketTimeout(30_000)
.enableLogging(true)
)
val client = HttpClient.defaultClient()
suspend fun fetchUsers(): Result<String> = runCatching {
val request = HttpRequest
.get("/users")
.queryParam("page", "1")
.header("Accept", "application/json")
.build()
val response = client.execute(request)
if (!response.isSuccessful) error("HTTP ${response.status}")
response.body ?: ""
}Main parameters in HttpClientConfig:
baseUrl: API base URL.connectTimeout: connection timeout (ms).socketTimeout: read/write timeout (ms).enableLogging: enable or disable logs.logLevel:NONE,BASIC,HEADERS,BODY.cache: cache configuration (CacheConfig).
POST with JSON body:
import kotlinx.serialization.Serializable
@Serializable
data class LoginRequest(val email: String, val password: String)
suspend fun login(email: String, password: String): Boolean {
val client = HttpClient.defaultClient()
val request = HttpRequest
.post("https://api.example.com")
.path("/auth/login")
.header("Content-Type", "application/json")
.body(LoginRequest(email, password))
.build()
val response = client.execute(request)
return response.isSuccessful
}Custom client with interceptors:
The library already provides some interceptors such as:
- AuthInterceptor: for authorization handling.
- TokenRefreshInterceptor: for token management.
- LoggingInterceptor: for customizing log output.
- ErrorHandlingInterceptor: for throwing exceptions based on HTTP error types.
import com.santimattius.http.config.HttpClientConfig
import com.santimattius.http.config.LogLevel
import com.santimattius.http.interceptor.LoggingInterceptor
val customClient = com.santimattius.http.HttpClient.create(
HttpClientConfig(baseUrl = "https://api.example.com")
.enableLogging(true)
.logLevel(LogLevel.BODY)
).addInterceptors(LoggingInterceptor())You can also implement your own interceptors using the Interceptor interface/protocol:
import KMPHttpClient
class OkHttpInterceptor: Interceptor {
func __intercept(chain: any InterceptorChain) async throws -> HttpResponse {
print("Hello from OkHttpInterceptor")
return try await chain.proceed(request: chain.request)
}
}Swift interop with JSON decoding:
import Foundation
import KMPHttpClient
struct User: Decodable { let id: Int; let name: String }
@MainActor
func loadUsers() async throws -> [User] {
let request = HttpRequest.companion.get("https://api.example.com")
.path("/users")
.header(name: "Accept", value: "application/json")
.build()
let client = HttpClient.shared.defaultClient()
let response = try await client.execute(request: request)
return try response.getBodyAs([User].self)
}- Initialize once with
HttpClient.initialize(...)and reuseHttpClient.defaultClient(). - Prefer a single
baseUrland build routes withget("/segment")andqueryParam(). - Adjust
connectTimeout(...)andsocketTimeout(...)according to your use case. - Use
enableLogging(true)andLogLevel.BODYonly in development to avoid leaking sensitive data. - Implement interceptors for auth, retries, and error handling (
com.santimattius.http.interceptor.*). - Always check
HttpResponse.isSuccessfuland handle errors properly. - Android: keep networking off the main thread (use coroutines and proper dispatchers).
- iOS: use
async/awaitand, if necessary, small Kotlin facades to simplify suspend calls from Swift.
Common pitfalls and how to avoid them:
- Forgetting
HttpClient.initialize(...)beforedefaultClient(): always initialize during app startup. - Malformed URLs: use
get(baseUrl).path("/segment")and validate. - Missing
Content-Typefor JSON: always setheader("Content-Type", "application/json"). - Excessive logging in production: limit to
BASICorNONE. - Suspend bridging issues in iOS: check interop configuration (Skie) or create Kotlin facades.
-
Source code:
- Core HTTP:
shared/src/commonMain/kotlin/com/santimattius/http/ - Swift extensions:
shared/src/commonMain/swift/ - Android sample:
androidApp/ - iOS sample:
iosApp/
- Core HTTP:
-
Official documentation:
- Kotlin Multiplatform: https://www.jetbrains.com/help/kotlin-multiplatform-dev/get-started.html
- Ktor Client: https://ktor.io/docs/getting-started-ktor-client.html
It is recommended to install and run kdoctor to verify that your development environment is correctly set up for Kotlin Multiplatform.
kdoctor helps diagnose and fix common configuration issues.