Skip to main content

Kotlin Multiplatform Networking

When building cross-platform applications with Kotlin Multiplatform, one of the most common requirements is making network requests to fetch data from remote servers or APIs. In this guide, we'll explore how to implement networking functionality that works seamlessly across different platforms.

Introduction to Cross-Platform Networking

In traditional development, you would use platform-specific networking libraries:

  • For Android: OkHttp, Retrofit, or Android's HttpURLConnection
  • For iOS: URLSession, Alamofire
  • For JavaScript: Fetch API, Axios

With Kotlin Multiplatform, we can write a single networking layer that works across all platforms. This approach:

  • Reduces code duplication
  • Ensures consistent behavior across platforms
  • Simplifies maintenance

The most popular networking library for KMP (Kotlin Multiplatform) is Ktor Client, which is designed specifically for Kotlin Multiplatform projects.

Setting Up Ktor Client

First, let's add the necessary dependencies to your project's build files:

kotlin
// In your shared module's build.gradle.kts
val commonMain by getting {
dependencies {
implementation("io.ktor:ktor-client-core:2.3.5")
implementation("io.ktor:ktor-client-content-negotiation:2.3.5")
implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.5")
}
}

val androidMain by getting {
dependencies {
implementation("io.ktor:ktor-client-android:2.3.5")
}
}

val iosMain by getting {
dependencies {
implementation("io.ktor:ktor-client-darwin:2.3.5")
}
}

The core dependencies provide the shared functionality, while platform-specific dependencies provide the actual implementation for each platform.

Creating a Basic HTTP Client

Let's create a simple HTTP client class in your common code:

kotlin
import io.ktor.client.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.json.Json

class NetworkClient {
val httpClient = HttpClient {
install(ContentNegotiation) {
json(Json {
prettyPrint = true
isLenient = true
ignoreUnknownKeys = true
})
}
}
}

This creates a basic HTTP client with JSON serialization/deserialization capabilities.

Making Your First Request

Now, let's make a simple GET request to fetch data from an API:

kotlin
import io.ktor.client.request.*
import io.ktor.client.statement.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow

class PostsRepository(private val networkClient: NetworkClient) {

suspend fun fetchPosts(): String {
val response: HttpResponse = networkClient.httpClient.get("https://jsonplaceholder.typicode.com/posts")
return response.bodyAsText()
}
}

Working with JSON Data

To properly handle JSON responses, we should define data classes and use Kotlin serialization:

kotlin
import kotlinx.serialization.Serializable
import io.ktor.client.call.body

@Serializable
data class Post(
val id: Int,
val userId: Int,
val title: String,
val body: String
)

class PostsRepository(private val networkClient: NetworkClient) {

suspend fun fetchPosts(): List<Post> {
return networkClient.httpClient.get("https://jsonplaceholder.typicode.com/posts").body()
}

suspend fun fetchPost(id: Int): Post {
return networkClient.httpClient.get("https://jsonplaceholder.typicode.com/posts/$id").body()
}
}

Handling Network Errors

Network requests can fail for various reasons. Here's how to handle errors:

kotlin
import io.ktor.client.plugins.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.flow

sealed class Result<out T> {
data class Success<T>(val data: T) : Result<T>()
data class Error(val exception: Throwable) : Result<Nothing>()
}

class PostsRepository(private val networkClient: NetworkClient) {

fun getPosts(): Flow<Result<List<Post>>> = flow {
try {
val posts = networkClient.httpClient.get("https://jsonplaceholder.typicode.com/posts").body<List<Post>>()
emit(Result.Success(posts))
} catch (e: Exception) {
emit(Result.Error(e))
}
}
}

This approach uses Kotlin Flow with a Result wrapper to propagate success or error states to the UI layer.

POST Requests and Request Parameters

Let's see how to make POST requests with request body:

kotlin
import io.ktor.client.request.*
import io.ktor.client.request.forms.*
import io.ktor.http.*

@Serializable
data class NewPost(val title: String, val body: String, val userId: Int)

class PostsRepository(private val networkClient: NetworkClient) {

suspend fun createPost(post: NewPost): Post {
return networkClient.httpClient.post("https://jsonplaceholder.typicode.com/posts") {
contentType(ContentType.Application.Json)
setBody(post)
}.body()
}
}

Authentication

Most real-world APIs require some form of authentication. Here's how to add an authentication header:

kotlin
import io.ktor.client.plugins.defaultRequest
import io.ktor.http.HttpHeaders

class AuthNetworkClient(private val apiKey: String) {
val httpClient = HttpClient {
install(ContentNegotiation) {
json()
}

defaultRequest {
headers {
append(HttpHeaders.Authorization, "Bearer $apiKey")
}
}
}
}

Creating a Complete Networking Layer

Now, let's put everything together to create a more complete networking layer:

kotlin
import io.ktor.client.*
import io.ktor.client.plugins.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.plugins.logging.*
import io.ktor.client.request.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.json.Json

class ApiClient(private val baseUrl: String, private val apiKey: String? = null) {

val client = HttpClient {
// Configure timeout
install(HttpTimeout) {
requestTimeoutMillis = 30000
connectTimeoutMillis = 15000
socketTimeoutMillis = 15000
}

// Add logging
install(Logging) {
level = LogLevel.HEADERS
}

// Add JSON serialization
install(ContentNegotiation) {
json(Json {
prettyPrint = true
isLenient = true
ignoreUnknownKeys = true
})
}

// Default headers for all requests
defaultRequest {
url(baseUrl)

apiKey?.let {
headers {
append("Authorization", "Bearer $it")
}
}
}
}
}

Real-World Example: Weather App

Let's build a simple weather API client as a practical example:

kotlin
import io.ktor.client.call.body
import io.ktor.client.request.get
import kotlinx.serialization.Serializable

@Serializable
data class WeatherResponse(
val location: Location,
val current: CurrentWeather
)

@Serializable
data class Location(
val name: String,
val region: String,
val country: String
)

@Serializable
data class CurrentWeather(
val temp_c: Float,
val temp_f: Float,
val condition: WeatherCondition
)

@Serializable
data class WeatherCondition(
val text: String,
val icon: String
)

class WeatherApiClient {
private val apiClient = ApiClient("https://api.weatherapi.com/v1", "YOUR_API_KEY")

suspend fun getCurrentWeather(city: String): WeatherResponse {
return apiClient.client.get("current.json") {
url {
parameters.append("q", city)
}
}.body()
}
}

// Using the API
suspend fun displayWeather(city: String) {
val weatherApi = WeatherApiClient()
try {
val weather = weatherApi.getCurrentWeather(city)
println("Current weather in ${weather.location.name}: ${weather.current.temp_c}°C, ${weather.current.condition.text}")
} catch(e: Exception) {
println("Error fetching weather: ${e.message}")
}
}

Output:

Current weather in London: 18.5°C, Partly cloudy

Platform-Specific Network Considerations

While Ktor Client provides a great cross-platform solution, sometimes you need platform-specific functionality:

Android Specific Configuration

kotlin
// In your androidMain sourceset
import io.ktor.client.engine.android.Android

actual fun createPlatformHttpClient(): HttpClient {
return HttpClient(Android) {
engine {
connectTimeout = 100_000
socketTimeout = 100_000
}
}
}

iOS Specific Configuration

kotlin
// In your iosMain sourceset
import io.ktor.client.engine.darwin.Darwin

actual fun createPlatformHttpClient(): HttpClient {
return HttpClient(Darwin) {
engine {
configureRequest {
setAllowsCellularAccess(true)
}
}
}
}

Best Practices for KMP Networking

  1. Use expected/actual for platform-specific features:

    kotlin
    // commonMain
    expect fun createHttpClient(): HttpClient

    // androidMain
    actual fun createHttpClient(): HttpClient = HttpClient(Android) { ... }

    // iosMain
    actual fun createHttpClient(): HttpClient = HttpClient(Darwin) { ... }
  2. Cache responses when appropriate to reduce API calls and improve offline experience

  3. Use dependency injection to provide HTTP clients to repositories

  4. Handle connectivity issues gracefully with appropriate user feedback

  5. Separate API interfaces from implementation details

Summary

In this guide, we've explored how to implement cross-platform networking in Kotlin Multiplatform applications using Ktor Client. We've covered:

  • Setting up Ktor Client in a Kotlin Multiplatform project
  • Making GET and POST requests
  • Handling JSON serialization/deserialization
  • Error handling strategies
  • Authentication
  • Building a complete networking layer
  • Platform-specific configuration

With these tools and techniques, you can build robust networking functionality that works across all platforms supported by your Kotlin Multiplatform application.

Additional Resources

  1. Ktor Client Official Documentation
  2. Kotlin Serialization Guide
  3. Handling API Responses with Coroutines

Exercises

  1. Extend the WeatherAPI client to fetch a 5-day forecast
  2. Implement a simple GitHub API client that can fetch user profiles
  3. Create a generic error handling system with different error types (network error, API error, etc.)
  4. Add request caching to prevent redundant API calls
  5. Implement a retry mechanism for failed network requests

By mastering networking in Kotlin Multiplatform, you'll be able to create applications that seamlessly communicate with web services regardless of the platform they run on.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)