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:
// 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:
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:
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:
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:
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:
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:
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:
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:
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
// 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
// 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
-
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) { ... } -
Cache responses when appropriate to reduce API calls and improve offline experience
-
Use dependency injection to provide HTTP clients to repositories
-
Handle connectivity issues gracefully with appropriate user feedback
-
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
- Ktor Client Official Documentation
- Kotlin Serialization Guide
- Handling API Responses with Coroutines
Exercises
- Extend the WeatherAPI client to fetch a 5-day forecast
- Implement a simple GitHub API client that can fetch user profiles
- Create a generic error handling system with different error types (network error, API error, etc.)
- Add request caching to prevent redundant API calls
- 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! :)