Compose Multiplatform Networking: Kotlin HttpClient Guide

Building robust and scalable applications often involves handling networking requests to fetch data, send updates, or interact with various APIs. Jetpack Compose simplifies UI development on Android, but for more complex networking across multiple platforms (like iOS, Desktop, and Web), Compose Multiplatform is essential. This article will explore implementing network requests using Kotlin Multiplatform in conjunction with Jetpack Compose.

Understanding Compose Multiplatform and Networking

Compose Multiplatform, powered by Kotlin, enables you to write UI code that can be shared across different platforms. For networking in such an environment, you need a strategy that works consistently regardless of the underlying platform. Kotlin’s `HttpClient` library provides a versatile solution that we can leverage effectively.

Why Use Kotlin’s HttpClient?

  • Multiplatform Support: Works seamlessly on Android, iOS, Desktop, and Web.
  • Type-safe Requests: Provides mechanisms for handling serialization and deserialization with Kotlin’s type system.
  • Asynchronous Operations: Supports asynchronous request processing using Kotlin Coroutines, crucial for maintaining responsive UIs.

Implementation Overview

  1. Project Setup: Configure a multiplatform project in IntelliJ IDEA or Android Studio.
  2. Dependency Inclusion: Add the necessary dependencies for the Kotlin HttpClient library in your build.gradle.kts (or build.gradle) files.
  3. Create a Network Service: Implement a service that encapsulates the network requests, using coroutines for asynchronous operations.
  4. Integrate into Compose UI: Use Compose’s rememberCoroutineScope to initiate network requests from your composable functions and handle the data using state management.

Step-by-Step Guide

Step 1: Project Setup

Ensure you have a Compose Multiplatform project configured. This usually involves creating a new project in IntelliJ IDEA and selecting the multiplatform template.

Step 2: Add Dependencies

In your shared/build.gradle.kts file, add the necessary dependencies for networking.


plugins {
    kotlin("multiplatform") version "1.9.0"
    kotlin("plugin.serialization") version "1.9.0"
    id("org.jetbrains.compose") version "1.5.1"
}

group = "org.example"
version = "1.0-SNAPSHOT"

repositories {
    mavenCentral()
}

kotlin {
    jvm {
        compilations.all {
            kotlinOptions.jvmTarget = "11"
        }
        testRuns["test"].executionTask.configure {
            useJUnitPlatform()
        }
    }
    js(IR) {
        browser()
        nodejs()
    }
    iosX64()
    iosArm64()
    iosSimulatorArm64()

    sourceSets {
        val commonMain by getting {
            dependencies {
                implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1")
                implementation("io.ktor:ktor-client-core:2.3.7")
                implementation("io.ktor:ktor-client-serialization:2.3.7")
                implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2")
            }
        }
        val commonTest by getting {
            dependencies {
                implementation(kotlin("test"))
                implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.1")
            }
        }
        val jvmMain by getting {
            dependencies {
                implementation("io.ktor:ktor-client-java:2.3.7")
                implementation("org.slf4j:slf4j-simple:2.0.9")
            }
        }
        val jsMain by getting {
            dependencies {
                implementation("io.ktor:ktor-client-js:2.3.7")
            }
        }
        val iosX64Main by getting
        val iosArm64Main by getting
        val iosSimulatorArm64Main by getting {
            dependencies {
                implementation("io.ktor:ktor-client-ios:2.3.7")
            }
        }
    }
}

compose {
    kotlinCompilerPlugin.set("1.5.1")
}

These dependencies include:

  • Kotlinx Coroutines: For handling asynchronous tasks.
  • Ktor HttpClient: A multiplatform HTTP client.
  • Kotlinx Serialization: For JSON serialization and deserialization.

Step 3: Create a Network Service

Create a NetworkService object in the commonMain source set. This service will encapsulate your network requests.


import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.request.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.json.Json
import io.ktor.http.*
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.withContext
import kotlinx.serialization.Serializable

@Serializable
data class ApiResponse(val fact: String)

object NetworkService {
    private val httpClient = HttpClient {
        install(ContentNegotiation) {
            json(Json {
                ignoreUnknownKeys = true
                isLenient = true
            })
        }
    }

    suspend fun fetchFact(): ApiResponse? {
        return safeApiCall {
            httpClient.get("https://catfact.ninja/fact").body()
        }
    }


    private suspend fun <T> safeApiCall(
        dispatcher: CoroutineDispatcher = Dispatchers.IO,
        apiCall: suspend () -> T
    ): T? {
        return withContext(dispatcher) {
            try {
                apiCall()
            } catch (e: Exception) {
                println("API call failed: ${e.message}")
                null
            }
        }
    }

}

Explanation:

  • The HttpClient is configured with a JSON serializer/deserializer.
  • The fetchFact function uses httpClient.get to make a GET request and deserialize the response.
  • Uses a safeApiCall that takes the function for networking call to reduce boilerplate code in multiple functions to implement network requests

Step 4: Integrate into Compose UI

In your platform-specific code (e.g., Android’s MainActivity or a Compose Desktop application), use Compose to display the fetched data.


import androidx.compose.foundation.layout.*
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.singleWindowApplication
import kotlinx.coroutines.launch

@Composable
fun FactView() {
    var fact by remember { mutableStateOf("") }
    val coroutineScope = rememberCoroutineScope()

    Column(
        modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text(text = "Fact: $fact")
        Spacer(modifier = Modifier.height(16.dp))
        Button(onClick = {
            coroutineScope.launch {
                val apiResponse = NetworkService.fetchFact()
                fact = apiResponse?.fact ?: "Failed to fetch fact"
            }
        }) {
            Text("Fetch New Fact")
        }
    }
}


fun main() = singleWindowApplication {
    FactView()
}

Key points:

  • State Management: Uses mutableStateOf to hold the fact and trigger recompositions when the data changes.
  • Coroutine Scope: rememberCoroutineScope provides a scope for launching coroutines within the composable lifecycle.
  • Button Action: When the button is clicked, it launches a coroutine to fetch a new fact and update the state.

Testing and Debugging

  • Unit Tests: Write unit tests using mock responses to ensure the network service works as expected.
  • Logging: Utilize Kotlin’s logging libraries to monitor network requests and responses.
  • Debugging Tools: Leverage platform-specific debugging tools to inspect network traffic.

Conclusion

Leveraging Compose Multiplatform alongside Kotlin’s HttpClient allows for the efficient development of UI applications that require robust networking capabilities across various platforms. This setup ensures maintainability, reusability, and type-safe request handling, essential for any scalable application architecture. Following this guide should set you on the path to build seamless multiplatform applications that fetch, process, and display data effectively, no matter where your application runs.