KMP Networking
Communityahmed3elshaer
Ktor client for Kotlin Multiplatform. Shared networking layer with platform-specific engines (OkHttp for Android, Darwin for iOS).
Overview
- Repository
- ahmed3elshaer/everything-claude-code-mobile
- Publisher
- ahmed3elshaer
- Stars
- 66
- Forks
- 18
- Bundled files
- 0
- License
- MIT
- Updated
- Jun 14, 2026
Installation
tmp=$(mktemp -d)
git clone --depth 1 https://github.com/ahmed3elshaer/everything-claude-code-mobile.git "$tmp"
mkdir -p ~/.claude/skills
cp -R "$tmp/skills/kmp-networking" ~/.claude/skills/kmp-networking
rm -rf "$tmp"tmp=$(mktemp -d)
git clone --depth 1 https://github.com/ahmed3elshaer/everything-claude-code-mobile.git "$tmp"
mkdir -p ~/.codex/skills
cp -R "$tmp/skills/kmp-networking" ~/.codex/skills/kmp-networking
rm -rf "$tmp"Install the KMP Networking agent skill from https://github.com/ahmed3elshaer/everything-claude-code-mobile/tree/main/skills/kmp-networking into this project's skills folder.Instructions
KMP Networking with Ktor
Configure Ktor client for cross-platform networking with platform-optimized engines.
Dependencies
// build.gradle.kts (shared module)
plugins {
kotlin("multiplatform")
kotlin("plugin.serialization")
}
kotlin {
sourceSets {
val commonMain by getting {
dependencies {
implementation("io.ktor:ktor-client-core:2.3.7")
implementation("io.ktor:ktor-client-content-negotiation:2.3.7")
implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.7")
implementation("io.ktor:ktor-client-logging:2.3.7")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")
}
}
val androidMain by getting {
dependencies {
implementation("io.ktor:ktor-client-okhttp:2.3.7")
}
}
val iosMain by getting {
dependencies {
implementation("io.ktor:ktor-client-darwin:2.3.7")
}
}
}
}HttpClient Factory
// commonMain/kotlin/network/HttpClientFactory.kt
object HttpClientFactory {
fun create(
platform: Platform,
isDebug: Boolean = false
): HttpClient {
return HttpClient(createEngine(platform)) {
install(ContentNegotiation) {
json(Json {
ignoreUnknownKeys = true
isLenient = true
encodeDefaults = false
})
}
if (isDebug) {
install(Logging) {
level = LogLevel.INFO
logger = object : Logger {
override fun log(message: String) {
println("Ktor: $message")
}
}
}
}
install(Auth) {
bearer {
loadTokens {
// Access Token from secure storage
BearerTokens(
accessTokenStorage.get() ?: "",
refreshTokenStorage.get() ?: ""
)
}
refreshTokens {
// Refresh token logic
val newTokens = authApi.refreshToken()
accessTokenStorage.save(newTokens.accessToken)
refreshTokenStorage.save(newTokens.refreshToken)
BearerTokens(newTokens.accessToken, newTokens.refreshToken)
}
}
}
defaultRequest {
url {
protocol = URLProtocol.HTTPS
host = "api.example.com"
}
header("X-API-Version", "1.0")
header("X-Platform", platform.name)
}
expectSuccess = true
HttpResponseValidator {
handleResponseExceptionWithRequest { exception, request ->
when (exception) {
is ClientRequestException -> {
val statusCode = exception.response.status.value
when (statusCode) {
401 -> throw UnauthorizedException()
403 -> throw ForbiddenException()
404 -> throw NotFoundException()
in 500..599 -> throw ServerException()
}
}
is ServerResponseException -> throw ServerException()
}
}
}
install(ResponseObserver) {
onResponse { response ->
// Track response times, errors
}
}
}
}
private fun createEngine(platform: Platform): HttpClientEngine {
return when (platform) {
Platform.ANDROID -> createOkhttpEngine()
Platform.IOS -> createDarwinEngine()
}
}
}Platform-Specific Engines
Android (OkHttp)
// androidMain/kotlin/network/OkHttpEngineFactory.kt
fun createOkhttpEngine(): OkHttpEngine {
val config = OkHttpConfig {
preconfigured = OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.addInterceptor { chain ->
val request = chain.request().newBuilder()
.header("User-Agent", "Android App/1.0")
.build()
chain.proceed(request)
}
.addInterceptor(HttpLoggingInterceptor().apply {
level = if (BuildConfig.DEBUG) {
HttpLoggingInterceptor.Level.BODY
} else {
HttpLoggingInterceptor.Level.NONE
}
})
.cache(
Cache(
File(context.cacheDir, "http_cache"),
10 * 1024 * 1024 // 10MB
)
)
.build()
}
return OkHttpEngine(config)
}iOS (Darwin)
// iosMain/kotlin/network/DarwinEngineFactory.kt
fun createDarwinEngine(): DarwinEngine {
val config = DarwinClientConfig {
configureSession {
setAllowsCellularAccess(true)
setAllowsExpensiveNetworkAccess(true)
setAllowsConstrainedNetworkAccess(true)
// Configure timeout
setTimeoutIntervalForRequest(30.0)
setTimeoutIntervalForResource(60.0)
// Configure cache
URLCache(
sharedCacheDirectory,
10 * 1024 * 1024 // 10MB
).let {
URLCache.setSharedURLCache(it)
}
}
}
return DarwinEngine(config)
}API Service Pattern
// commonMain/kotlin/network/api/UserApi.kt
class UserApi(
private val client: HttpClient
) {
suspend fun getUsers(page: Int = 1): PaginatedResponse<User> {
return client.get("/users") {
parameter("page", page)
parameter("limit", 20)
}.body()
}
suspend fun getUser(id: String): User {
return client.get("/users/$id").body()
}
suspend fun createUser(request: CreateUserRequest): User {
return client.post("/users") {
setBody(request)
contentType(ContentType.Application.Json)
}.body()
}
suspend fun updateUser(id: String, request: UpdateUserRequest): User {
return client.put("/users/$id") {
setBody(request)
contentType(ContentType.Application.Json)
}.body()
}
suspend fun deleteUser(id: String) {
return client.delete("/users/$id")
}
suspend fun uploadAvatar(userId: String, file: ByteArray): String {
return client.submitFormWithBinaryData(
url = "https://api.example.com/users/$userId/avatar",
formData = formData {
append("avatar", file, Headers.build {
append(HttpHeaders.ContentDisposition, "filename=avatar.jpg")
})
}
).body()
}
}Network Exceptions
// commonMain/kotlin/network/NetworkExceptions.kt
sealed class NetworkException(message: String? = null) : Exception(message)
class UnauthorizedException : NetworkException("User not authenticated")
class ForbiddenException : NetworkException("Access forbidden")
class NotFoundException : NetworkException("Resource not found")
class ServerException : NetworkException("Server error occurred")
class NetworkUnavailableException : NetworkException("Network unavailable")
class TimeoutException : NetworkException("Request timeout")
// Wrap Ktor exceptions
fun Throwable.toNetworkException(): NetworkException {
return when (this) {
is NetworkException -> this
is ClientRequestException -> when (response.status.value) {
401 -> UnauthorizedException()
403 -> ForbiddenException()
404 -> NotFoundException()
else -> NetworkException(message)
}
is ServerResponseException -> ServerException()
is HttpRequestTimeoutException -> TimeoutException()
is UnreachableAddressException,
is ConnectTimeoutException -> NetworkUnavailableException()
else -> NetworkException(message ?: "Unknown network error")
}
}Result Wrapper
// commonMain/kotlin/network/ApiResult.kt
sealed class ApiResult<out T> {
data class Success<T>(val data: T) : ApiResult<T>()
data class Error(val error: NetworkException) : ApiResult<Nothing>()
suspend fun <R> map(transform: (T) -> R): ApiResult<R> = when (this) {
is Success -> Success(transform(data))
is Error -> this
}
suspend fun <R> flatMap(transform: (T) -> ApiResult<R>): ApiResult<R> = when (this) {
is Success -> transform(data)
is Error -> this
}
fun getOrNull(): T? = when (this) {
is Success -> data
is Error -> null
}
fun getOrElse(defaultValue: T): T = when (this) {
is Success -> data
is Error -> defaultValue
}
}
suspend fun <T> apiCall(block: suspend () -> T): ApiResult<T> = try {
ApiResult.Success(block())
} catch (e: Exception) {
ApiResult.Error(e.toNetworkException())
}
// Usage
val result: ApiResult<User> = apiCall { userApi.getUser("123") }
when (result) {
is ApiResult.Success -> showUser(result.data)
is ApiResult.Error -> showError(result.error)
}Retry Logic
// commonMain/kotlin/network/Retry.kt
suspend fun <T> retryApiCall(
maxRetries: Int = 3,
delayMs: Long = 1000,
block: suspend () -> T
): T {
var lastException: Exception? = null
repeat(maxRetries) { attempt ->
try {
return block()
} catch (e: Exception) {
lastException = e
if (e is NetworkUnavailableException || e is TimeoutException) {
if (attempt < maxRetries - 1) {
delay(delayMs * (attempt + 1))
}
} else {
throw e
}
}
}
throw lastException ?: RuntimeException("Max retries exceeded")
}Offline Support
// commonMain/kotlin/network/OfflineCapableApi.kt
class OfflineCapableApi<T : Any>(
private val api: T,
private val cache: DatabaseCache
) : OfflineCapableApi<T> by api {
suspend fun <R> withCache(
key: String,
ttl: Duration,
block: suspend () -> R
): R = withContext(Dispatchers.IO) {
// Try cache first
cache.get<R>(key)?.let { cached ->
if (cached.timestamp + ttl.toMillisMilliseconds() > Clock.System.now()) {
return@withContext cached.data
}
}
// Fetch from network
try {
val result = block()
cache.put(key, CachedData(result, Clock.System.now()))
result
} catch (e: NetworkException) {
// Return stale cache if network fails
cache.get<R>(key)?.data ?: throw e
}
}
}Dependency Injection Setup
// commonMain/kotlin/di/NetworkModule.kt
val networkModule = module {
single { HttpClientFactory.create(get(), get()) }
single { UserApi(get()) }
single { AuthApi(get()) }
factory { ConnectivityMonitor(get()) }
}Best Practices
✅ DO
// ✅ Use typed API services
class UserApi(private val client: HttpClient)
// ✅ Wrap calls in result types
suspend fun getUser(): ApiResult<User>
// ✅ Configure timeouts
config { setTimeoutIntervalForRequest(30.0) }
// ✅ Add logging for debug builds
if (isDebug) { install(Logging) }
// ✅ Handle exceptions at boundaries
try { api.call() } catch (e: NetworkException) { /* handle */ }❌ DON'T
// ❌ Don't create multiple HttpClient instances
// Use singleton via DI
// ❌ Don't block on suspend calls
runBlocking { api.call() } // ❌
// ❌ Don't ignore exceptions
try { api.call() } catch (e: Exception) { } // ❌
// ❌ Don't hardcode URLs
client.get("https://api.example.com/users") // ❌
// Configure base URL in defaultRequestRemember: Networking is the bridge between your app and the world. Make it robust, testable, and platform-optimized.
FAQ
What does KMP Networking do?
Ktor client for Kotlin Multiplatform. Shared networking layer with platform-specific engines (OkHttp for Android, Darwin for iOS).
Who made KMP Networking?
ahmed3elshaer (a community member) publishes it in the ahmed3elshaer/everything-claude-code-mobile repository on GitHub.
Is KMP Networking free?
Yes. KMP Networking is open source, published by ahmed3elshaer under the MIT license.
What comes with KMP Networking?
Just its instructions. Everything the agent needs is in one SKILL.md file.
Which agents can use KMP Networking?
Any coding agent that reads SKILL.md files, including Claude Code, Codex, Cursor, GitHub Copilot and Gemini CLI. The agent loads the skill when a task matches its description.
What are agent skills?
An agent skill is a folder with a SKILL.md file: instructions that teach a coding agent how to do one kind of task, and optionally files it can read while it works. The agent keeps only the skill's name and description in mind and reads the rest when a task calls for it.
How are agent skills different from MCP servers?
An MCP server gives an agent new tools to call, code that runs somewhere and returns results. A skill gives it knowledge: how to approach a task with the tools it already has. Many agents use both.
How can my agent test my Android app?
Robomori lets coding agents create, start and control Android emulators and devices from one command, so they can install your app, take screenshots and check dark mode, font sizes and languages.