Aplikasi modern selalu membutuhkan HTTP client—untuk panggilan REST API, unduh file, atau WebSocket. Di proyek native, kita menulis kode HTTP berbeda per platform: OkHttp di Android, NSURLSession di iOS, HttpURLConnection di JVM. KMP dengan ktor-client menyederhanakan ini: kode HTTP ditulis sekali di commonMain, engine dipilih per platform. Pada chapter ini kita akan melihat arsitektur ktor-client, cara mendeklarasikan engine per source set, dan demonstrasi tiga engine berbeda melayani satu API service.
Arsitektur ktor-client #
ktor-client terdiri dari dua lapisan. Lapisan atas berisi kode HTTP yang bersama—HttpClient, plugin seperti ContentNegotiation, Auth, Logging. Lapisan bawah berisi engine yang spesifik platform—OkHttp untuk JVM dan Android, Darwin untuk iOS, Js untuk browser. Konsumen kode commonMain cukup memakai HttpClient tanpa peduli engine apa yang aktif.
import io.ktor.client.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
class ApiService(private val client: HttpClient) {
suspend fun ambilPengguna(id: Int): String {
return client.get("https://api.example.com/pengguna/$id").bodyAsText()
}
}Lapisan atas ini sama di semua target. Engine yang dipilih saat konstruksi HttpClient menentukan implementasi bawah.
Engine per Platform #
Engine ditentukan saat konstruksi HttpClient. Konfigurasi ini biasanya berada di source set spesifik karena setiap engine memiliki class yang berbeda.
import io.ktor.client.engine.okhttp.OkHttp
fun buatHttpClient(): HttpClient = HttpClient(OkHttp) {
engine { config { retryOnConnectionFailure(true) } }
}import io.ktor.client.engine.darwin.Darwin
fun buatHttpClient(): HttpClient = HttpClient(Darwin) {
engine { configureRequest { setAllowsCellularAccess(true) } }
}Engine yang dipilih otomatis mengimplementasikan interface HttpEngineFactory di ktor-client-core. Kode commonMain hanya melihat HttpClient, tidak peduli engine.
Simulasi HTTP Client Multiplatform #
Demo berikut mensimulasikan arsitektur ktor-client dengan interface HttpEngine dan tiga implementasi. Pemilihan engine dilakukan secara eksplisit di main() untuk simulasi; di proyek nyata, ini dilakukan per source set via expect/actual.
// multiplatform_networking.kt
// ktor-client untuk KMP: HTTP code di commonMain, engine per platform.
// Demo: simulasi HTTP client dengan engine engine-interface + 3 implementasi.
// --- commonMain: abstraksi engine dan HTTP client ---
sealed class HttpRespon {
data class Sukses(val status: Int, val body: String) : HttpRespon()
data class Gagal(val status: Int, val pesan: String) : HttpRespon()
data class Error(val exception: Throwable) : HttpRespon()
}
data class HttpPermintaan(
val url: String,
val method: String = "GET",
val headers: Map<String, String> = emptyMap(),
val body: String? = null
)
interface HttpEngine {
val nama: String
fun kirim(permintaan: HttpPermintaan): HttpRespon
}
// Ktor Client: kelas di commonMain yang memakai engine
class KtorClient(private val engine: HttpEngine) {
fun get(url: String, headers: Map<String, String> = emptyMap()): HttpRespon {
return engine.kirim(HttpPermintaan(url, "GET", headers))
}
fun post(url: String, body: String, headers: Map<String, String> = emptyMap()): HttpRespon {
return engine.kirim(HttpPermintaan(url, "POST", headers, body))
}
fun getJson(url: String): HttpRespon {
return get(url, mapOf("Accept" to "application/json"))
}
}
// --- commonMain: logika bisnis HTTP ---
class ApiService(private val client: KtorClient, private val baseUrl: String) {
data class Pengguna(val id: Int, val nama: String)
fun ambilPengguna(id: Int): Pengguna? {
val resp = client.getJson("$baseUrl/api/pengguna/$id")
return when (resp) {
is HttpRespon.Sukses -> parsePengguna(resp.body)
else -> null
}
}
private fun parsePengguna(body: String): Pengguna? {
// Parser manual sederhana untuk JSON {"id":1,"nama":"Budi"}
val idMatch = Regex("\"id\"\\s*:\\s*(\\d+)").find(body)
val namaMatch = Regex("\"nama\"\\s*:\\s*\"([^\"]+)\"").find(body)
if (idMatch != null && namaMatch != null) {
return Pengguna(idMatch.groupValues[1].toInt(), namaMatch.groupValues[1])
}
return null
}
}
// --- Platform engines ---
class OkHttpEngine(private val mockResponses: Map<String, String>) : HttpEngine {
override val nama: String = "OkHttp"
override fun kirim(permintaan: HttpPermintaan): HttpRespon {
// Di JVM/Android nyata: okhttp3.OkHttpClient.newCall(Request)
println(" [OkHttp] ${permintaan.method} ${permintaan.url}")
val body = mockResponses[permintaan.url]
return if (body != null) {
HttpRespon.Sukses(200, body)
} else {
HttpRespon.Gagal(404, "Not Found")
}
}
}
class DarwinEngine(private val mockResponses: Map<String, String>) : HttpEngine {
override val nama: String = "Darwin"
override fun kirim(permintaan: HttpPermintaan): HttpRespon {
// Di iOS nyata: Foundation.NSURLSession
println(" [Darwin] ${permintaan.method} ${permintaan.url}")
val body = mockResponses[permintaan.url]
return if (body != null) {
HttpRespon.Sukses(200, body)
} else {
HttpRespon.Gagal(404, "Not Found")
}
}
}
class MockEngine(private val mockResponses: Map<String, String>) : HttpEngine {
override val nama: String = "Mock"
override fun kirim(permintaan: HttpPermintaan): HttpRespon {
println(" [Mock] ${permintaan.method} ${permintaan.url}")
val body = mockResponses[permintaan.url]
return if (body != null) {
HttpRespon.Sukses(200, body)
} else {
HttpRespon.Gagal(404, "Not Found")
}
}
}
// --- Demo ---
fun main() {
val mockResponses = mapOf(
"https://api.example.com/api/pengguna/1" to """{"id":1,"nama":"Budi"}""",
"https://api.example.com/api/pengguna/2" to """{"id":2,"nama":"Siti"}""",
"https://api.example.com/api/pengguna/3" to """{"id":3,"nama":"Dewi"}"""
)
val engines = listOf<Pair<String, HttpEngine>>(
"JVM" to OkHttpEngine(mockResponses),
"Android" to OkHttpEngine(mockResponses),
"iOS" to DarwinEngine(mockResponses)
)
println("=== Multiplatform HTTP client (ktor-client simulasi) ===\n")
for ((platform, engine) in engines) {
println("--- Platform: $platform ---")
val client = KtorClient(engine)
val api = ApiService(client, "https://api.example.com")
for (id in 1..3) {
val pengguna = api.ambilPengguna(id)
println(" Pengguna(id=$id) = $pengguna")
}
// Test 404
val tidakAda = api.ambilPengguna(999)
println(" Pengguna(id=999) = $tidakAda")
println()
}
// Demo test dengan MockEngine
println("--- Pengujian dengan MockEngine ---")
val testClient = KtorClient(MockEngine(mockResponses))
val testApi = ApiService(testClient, "https://api.example.com")
val uji = testApi.ambilPengguna(1)
println(" Hasil test: $uji")
val ujiNotFound = testApi.ambilPengguna(999)
println(" Test 404: ${ujiNotFound ?: "(null sesuai harapan)"}")
}Output yang Dihasilkan:
=== Multiplatform HTTP client (ktor-client simulasi) ===
--- Platform: JVM ---
[OkHttp] GET https://api.example.com/api/pengguna/1
Pengguna(id=1) = Pengguna(id=1, nama=Budi)
[OkHttp] GET https://api.example.com/api/pengguna/2
Pengguna(id=2) = Pengguna(id=2, nama=Siti)
[OkHttp] GET https://api.example.com/api/pengguna/3
Pengguna(id=3) = Pengguna(id=3, nama=Dewi)
[OkHttp] GET https://api.example.com/api/pengguna/999
Pengguna(id=999) = null
--- Platform: Android ---
[OkHttp] GET https://api.example.com/api/pengguna/1
Pengguna(id=1) = Pengguna(id=1, nama=Budi)
[OkHttp] GET https://api.example.com/api/pengguna/2
Pengguna(id=2) = Pengguna(id=2, nama=Siti)
[OkHttp] GET https://api.example.com/api/pengguna/3
Pengguna(id=3) = Pengguna(id=3, nama=Dewi)
[OkHttp] GET https://api.example.com/api/pengguna/999
Pengguna(id=999) = null
--- Platform: iOS ---
[Darwin] GET https://api.example.com/api/pengguna/1
Pengguna(id=1) = Pengguna(id=1, nama=Budi)
[Darwin] GET https://api.example.com/api/pengguna/2
Pengguna(id=2) = Pengguna(id=2, nama=Siti)
[Darwin] GET https://api.example.com/api/pengguna/3
Pengguna(id=3) = Pengguna(id=3, nama=Dewi)
[Darwin] GET https://api.example.com/api/pengguna/999
Pengguna(id=999) = null
--- Pengujian dengan MockEngine ---
[Mock] GET https://api.example.com/api/pengguna/1
Hasil test: Pengguna(id=1, nama=Budi)
[Mock] GET https://api.example.com/api/pengguna/999
Test 404: (null sesuai harapan)Mari kita bedah bagian kunci dari kode di atas:
sealed class HttpRespon: model tiga kemungkinan hasil HTTP—Sukses(200, body),Gagal(404, "Not Found"),Error(exception). Pola sealed memaksa pemanggil menangani semua kasus diwhen.data class HttpPermintaan: model untuk satu request—URL, method, headers, body. Immutable sehingga aman dipassing antar thread.interface HttpEngine: abstraksi engine dicommonMain. Hanya satu methodkirim(permintaan): HttpRespon. Engine spesifik platform (OkHttpEngine,DarwinEngine) mengimplementasikan interface ini.class KtorClient: HTTP client yang menerima engine via constructor. Memiliki methodget,post,getJson—semuanya mendelegasikan keengine.kirim(). Padaktor-clientnyata, class ini juga memuat plugin pipeline (ContentNegotiation, Auth, Logging).class ApiService: kode bisnis dicommonMain. MemakaiKtorClientuntuk GET request, lalu memparse JSON respons menjadiPengguna. Pada proyek nyata, parsing digantikan pluginContentNegotiation+kotlinx.serialization.OkHttpEngine,DarwinEngine,MockEngine: tiga implementasi engine. Di JVM/Android nyata,OkHttpEnginememakaiokhttp3.OkHttpClient; di iOS nyata,DarwinEnginememakaiNSURLSession;MockEngineuntuk testing.- Output menunjukkan tiga platform: kode
ApiService.ambilPengguna()yang sama menghasilkan struktur respons identik—hanya prefix log engine yang berbeda ([OkHttp]vs[Darwin]).
Konfigurasi Gradle #
Library ktor-client dipublikasi sebagai beberapa artifact. commonMain membutuhkan ktor-client-core dan plugin seperti ktor-client-content-negotiation. Source set spesifik menambah engine: ktor-client-okhttp untuk jvmMain dan androidMain, ktor-client-darwin untuk iosMain.
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")
}
}
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") }
}
val jvmMain by getting {
dependencies { implementation("io.ktor:ktor-client-okhttp:2.3.7") }
}
}Engine tidak pernah ditambahkan ke commonMain—engine selalu spesifik platform.
Referensi Lanjutan #
- Ktor client create and configure — dokumentasi resmi Ktor untuk membuat dan mengonfigurasi Ktor Client multiplatform
Penutup #
ktor-client mengikuti pola klasik KMP—kode HTTP bersama di commonMain, engine spesifik per platform. Pemisahan ini membuat kode bisnis dapat diuji dengan MockEngine, sementara produksi memakai engine native terbaik. Chapter berikutnya akan masuk ke persistence: bagaimana SQLDelight dan Room KMP menyediakan akses database lintas platform dengan driver spesifik.