Back to Blog

Setting Up Ktor for Networking in Kotlin Multiplatform Projects

Modern mobile apps rely heavily on remote APIs. In traditional cross-platform or separate native developments, teams write parallel networking boilerplate (JSON parsing, auth headers, retry policies) for both Android and iOS.

With Kotlin Multiplatform (KMP), you can write your networking logic once inside the shared codebase and use it directly on both platforms. The premier library for this is Ktor—an asynchronous HTTP client built from the ground up for Kotlin and coroutines.


Why Ktor?

Unlike platform-dependent libraries like Retrofit (which runs on the JVM and cannot compile to iOS native), Ktor is built with Multiplatform support in mind. It uses a pluggable engine architecture:

  • On Android, it can use the OkHttp or standard Android engine.
  • On iOS, it compiles to native code and uses the Darwin engine under the hood.

1. Adding Dependencies

To get started, add the Ktor dependencies inside your shared module's build.gradle.kts file:

plugins {
    kotlin("multiplatform")
    kotlin("plugin.serialization") version "2.0.0" // Ensure serialization plugin is active
}

kotlin {
    sourceSets {
        commonMain.dependencies {
            // Core Ktor Client
            implementation("io.ktor:ktor-client-core:2.3.12")
            // Content Negotiation (JSON formatting)
            implementation("io.ktor:ktor-client-content-negotiation:2.3.12")
            implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.12")
            // Logging plugin for network debugging
            implementation("io.ktor:ktor-client-logging:2.3.12")
        }
        androidMain.dependencies {
            // Android engine
            implementation("io.ktor:ktor-client-okhttp:2.3.12")
        }
        iosMain.dependencies {
            // iOS engine
            implementation("io.ktor:ktor-client-darwin:2.3.12")
        }
    }
}

2. Configuring the Ktor Client

Ktor uses a builder pattern to configure its engine and install plugins. In your common code (commonMain/kotlin), construct a single HTTP client:

import io.ktor.client.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.plugins.logging.*
import io.ktor.client.plugins.defaultRequest
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.json.Json

val httpClient = HttpClient {
    // 1. JSON Content Negotiation
    install(ContentNegotiation) {
        json(Json {
            prettyPrint = true
            isLenient = true
            ignoreUnknownKeys = true // Prevents crashes when API sends undocumented fields
        })
    }

    // 2. Logging HTTP Calls
    install(Logging) {
        logger = Logger.DEFAULT
        level = LogLevel.INFO
    }

    // 3. Default Configuration (Headers & Base URL)
    defaultRequest {
        url("https://api.github.com/")
        header(HttpHeaders.ContentType, ContentType.Application.Json)
    }
}

Note: In production architectures, you would inject this client using a Dependency Injection container like Koin.


3. Defining Data Models

For type-safe deserialization, annotate your model classes with @Serializable from kotlinx.serialization:

import kotlinx.serialization.Serializable

@Serializable
data class GitHubUser(
    val id: Long,
    val login: String,
    val name: String?,
    val bio: String?,
    val publicRepos: Int
)

4. Making Async Network Calls

Because Ktor is built with Kotlin Coroutines, requests are naturally asynchronous. Create an API service or repository class to consume the endpoints:

import io.ktor.client.call.*
import io.ktor.client.request.*

class GitHubApiService(private val client: HttpClient = httpClient) {
    
    suspend fun fetchUserProfile(username: String): GitHubUser {
        return client.get("users/$username") {
            // Add query parameters or customize request here
        }.body()
    }
}

On Android, you can consume this suspending function in a standard ViewModel using viewModelScope. On iOS, Swift can call this suspending function directly (it translates into an async/await signature or completion handler in Swift).


5. Threading and Concurrency Best Practices

When calling shared networking code from iOS, Kotlin Coroutines must handle background dispatching correctly:

  1. Avoid Hardcoded Dispatchers: Don't hardcode Dispatchers.IO directly inside shared repositories. Instead, inject dispatchers or use the default coroutine context where possible.
  2. Swift Interop: Ensure your iOS targets run in a thread-safe environment. If you are using Kotlin 1.9+ or Kotlin 2.0 with the new memory manager, freezing variables is no longer required, making background coroutines much simpler on iOS.

Conclusion

By configuring Ktor once in the shared core module of your Kotlin Multiplatform application, you build a performant, type-safe API client that services both Android and iOS. This reduces surface area for bugs, ensures consistent network-level validation, and dramatically improves feature velocity.

Now that you have configured your networking layer, the next step in building an offline-first app is setting up local data persistence. Check out my guide on Data Persistence in Kotlin Multiplatform with SQLDelight to learn how to cache your API responses locally.


Need an Experienced KMP Developer?

Looking to integrate Ktor, SQLDelight caching, or Compose Multiplatform into your mobile stack? I offer remote consulting and contract development for engineering teams in Europe, the UK, and Canada, supporting CEST, GMT, and EST timezones.

Let's Connect & Discuss Your Project