Setelah memahami struktur source set dan pola kode platform-specific, kita perlu mengelola dependency eksternal. Library yang dipakai di commonMain harus kompatibel dengan semua target—tidak boleh hanya berjalan di JVM. Library platform-specific ditambahkan ke source set spesifik. Gradle mengelola transitivitas dependency: library A yang membutuhkan library B akan otomatis menarik B ke classpath. Pada chapter ini kita akan membedah deklarasi dependency di build.gradle.kts, melihat resolusi transitif, dan memahami perbedaan classpath antara commonMain, androidMain, iosMain, dan jvmMain.

Dependency commonMain: Hanya Library KMP #

Library yang ditambahkan ke commonMain harus mendukung multiplatform. Library seperti kotlinx-coroutines-core, kotlinx-serialization-json, kotlinx-datetime, dan ktor-client-core menyediakan artifact KMP yang berisi kode commonMain plus source set spesifik per target. Gradle akan otomatis memilih artifact yang sesuai untuk setiap target.

kotlin build.gradle.kts: dependency commonMain
kotlin {
    sourceSets {
        val commonMain by getting {
            dependencies {
                implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
                implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3")
                implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.6.0")
                implementation("io.ktor:ktor-client-core:2.3.7")
                implementation("io.ktor:ktor-client-content-negotiation:2.3.7")
            }
        }
    }
}

Library di commonMain otomatis tersedia di semua source set turunan—androidMain, iosMain, jvmMain tidak perlu mendeklarasikan ulang.

Dependency Platform-Specific #

Library yang belum mendukung KMP—misalnya okhttp, postgresql, atau lifecycle-viewmodel—harus ditambahkan ke source set spesifik. Gradle memastikan library jvmMain tidak bocor ke iosMain, sehingga kompilasi iOS tidak error karena mencoba memuat class JVM-only.

kotlin build.gradle.kts: dependency platform-specific
sourceSets {
    val androidMain by getting {
        dependencies {
            implementation("io.ktor:ktor-client-okhttp:2.3.7")
            implementation("androidx.lifecycle:lifecycle-viewmodel:2.6.2")
        }
    }
    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")
            implementation("org.postgresql:postgresql:42.6.0")
        }
    }
}

Simulasi Resolusi Dependency #

Demo berikut mensimulasikan bagaimana Gradle me-resolve dependency transitif per source set. Library ktor-client-content-negotiation bergantung pada ktor-client-core, yang otomatis ditarik ke classpath. Library ktor-client-okhttp bergantung pada ktor-client-core dan okhttp. Kode commonMain hanya melihat ktor-client-core; kode jvmMain dan androidMain melihat keduanya; kode iosMain hanya melihat ktor-client-darwin (plus ktor-client-core dari commonMain).

kotlin
// multiplatform_dependencies.kt
// Manajemen dependency KMP: commonMain (KMP-compatible) vs platform-specific.
// Demo: simulasi resolusi dependency tree per source set.

data class Artifact(
    val group: String,
    val name: String,
    val version: String,
    val targets: Set<String>, // target yang kompatibel, atau "common"
    val transitive: List<String> = emptyList()
) {
    fun notation(): String = "$group:$name:$version"
}

data class SourceSetDependencies(
    val sourceSet: String,
    val direct: List<String>, // artifact notations
    val inherited: List<String> = emptyList()
) {
    fun all(): List<String> = (inherited + direct).distinct().sorted()
}

class DependencyResolver(private val catalog: Map<String, Artifact>) {
    fun resolve(sourceSet: String, deps: List<String>): Set<String> {
        val result = mutableSetOf<String>()
        val queue = ArrayDeque(deps)
        while (queue.isNotEmpty()) {
            val notation = queue.removeFirst()
            if (notation in result) continue
            result.add(notation)
            val art = catalog[notation] ?: continue
            for (t in art.transitive) queue.addLast(t)
        }
        return result
    }
}

fun main() {
    val catalog = mutableMapOf<String, Artifact>()

    fun put(a: Artifact) { catalog[a.notation()] = a }

    put(Artifact("org.jetbrains.kotlinx", "kotlinx-coroutines-core", "1.7.3",
        setOf("common"),
        listOf("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")))
    put(Artifact("org.jetbrains.kotlinx", "kotlinx-serialization-json", "1.6.3",
        setOf("common"),
        listOf("org.jetbrains.kotlinx:kotlinx-serialization-core:1.6.3")))
    put(Artifact("org.jetbrains.kotlinx", "kotlinx-serialization-core", "1.6.3",
        setOf("common")))
    put(Artifact("org.jetbrains.kotlinx", "kotlinx-datetime", "0.6.0",
        setOf("common")))
    put(Artifact("io.ktor", "ktor-client-core", "2.3.7",
        setOf("common")))
    put(Artifact("io.ktor", "ktor-client-content-negotiation", "2.3.7",
        setOf("common"),
        listOf("io.ktor:ktor-client-core:2.3.7")))
    put(Artifact("com.squareup.okhttp3", "okhttp", "4.12.0",
        setOf("jvm", "android")))
    put(Artifact("io.ktor", "ktor-client-okhttp", "2.3.7",
        setOf("jvm", "android"),
        listOf("io.ktor:ktor-client-core:2.3.7", "com.squareup.okhttp3:okhttp:4.12.0")))
    put(Artifact("io.ktor", "ktor-client-darwin", "2.3.7",
        setOf("ios")))
    put(Artifact("androidx.lifecycle", "lifecycle-viewmodel", "2.6.2",
        setOf("android")))
    put(Artifact("org.postgresql", "postgresql", "42.6.0",
        setOf("jvm")))

    val resolver = DependencyResolver(catalog)

    val commonDeps = listOf(
        "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3",
        "org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3",
        "org.jetbrains.kotlinx:kotlinx-datetime:0.6.0",
        "io.ktor:ktor-client-content-negotiation:2.3.7"
    )
    val androidDeps = listOf(
        "io.ktor:ktor-client-okhttp:2.3.7",
        "androidx.lifecycle:lifecycle-viewmodel:2.6.2"
    )
    val iosDeps = listOf(
        "io.ktor:ktor-client-darwin:2.3.7"
    )
    val jvmDeps = listOf(
        "io.ktor:ktor-client-okhttp:2.3.7",
        "org.postgresql:postgresql:42.6.0"
    )

    val commonResolved = resolver.resolve("commonMain", commonDeps)
    val androidResolved = resolver.resolve("androidMain", androidDeps + commonDeps)
    val iosResolved = resolver.resolve("iosMain", iosDeps + commonDeps)
    val jvmResolved = resolver.resolve("jvmMain", jvmDeps + commonDeps)

    println("=== commonMain dependencies (final classpath) ===")
    commonResolved.sorted().forEach { println("  - $it") }
    println()

    println("=== androidMain dependencies (final classpath) ===")
    androidResolved.sorted().forEach { println("  - $it") }
    println()

    println("=== iosMain dependencies (final classpath) ===")
    iosResolved.sorted().forEach { println("  - $it") }
    println()

    println("=== jvmMain dependencies (final classpath) ===")
    jvmResolved.sorted().forEach { println("  - $it") }
    println()

    println("=== Ringkasan perbandingan ===")
    println("commonMain : ${commonResolved.size} artifacts")
    println("androidMain: ${androidResolved.size} artifacts (common + platform)")
    println("iosMain    : ${iosResolved.size} artifacts (common + platform)")
    println("jvmMain    : ${jvmResolved.size} artifacts (common + platform)")

    println()
    println("=== Source set spesifik (platform-only) ===")
    val androidOnly = androidResolved - commonResolved
    val iosOnly = iosResolved - commonResolved
    val jvmOnly = jvmResolved - commonResolved
    println("androidMain khusus: ${androidOnly.sorted()}")
    println("iosMain khusus    : ${iosOnly.sorted()}")
    println("jvmMain khusus    : ${jvmOnly.sorted()}")
}

Output yang Dihasilkan:

text
=== commonMain dependencies (final classpath) ===
  - io.ktor:ktor-client-content-negotiation:2.3.7
  - io.ktor:ktor-client-core:2.3.7
  - org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3
  - org.jetbrains.kotlinx:kotlinx-datetime:0.6.0
  - org.jetbrains.kotlinx:kotlinx-serialization-core:1.6.3
  - org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3

=== androidMain dependencies (final classpath) ===
  - androidx.lifecycle:lifecycle-viewmodel:2.6.2
  - com.squareup.okhttp3:okhttp:4.12.0
  - io.ktor:ktor-client-content-negotiation:2.3.7
  - io.ktor:ktor-client-core:2.3.7
  - io.ktor:ktor-client-okhttp:2.3.7
  - org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3
  - org.jetbrains.kotlinx:kotlinx-datetime:0.6.0
  - org.jetbrains.kotlinx:kotlinx-serialization-core:1.6.3
  - org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3

=== iosMain dependencies (final classpath) ===
  - io.ktor:ktor-client-content-negotiation:2.3.7
  - io.ktor:ktor-client-core:2.3.7
  - io.ktor:ktor-client-darwin:2.3.7
  - org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3
  - org.jetbrains.kotlinx:kotlinx-datetime:0.6.0
  - org.jetbrains.kotlinx:kotlinx-serialization-core:1.6.3
  - org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3

=== jvmMain dependencies (final classpath) ===
  - com.squareup.okhttp3:okhttp:4.12.0
  - io.ktor:ktor-client-content-negotiation:2.3.7
  - io.ktor:ktor-client-core:2.3.7
  - io.ktor:ktor-client-okhttp:2.3.7
  - org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3
  - org.jetbrains.kotlinx:kotlinx-datetime:0.6.0
  - org.jetbrains.kotlinx:kotlinx-serialization-core:1.6.3
  - org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3
  - org.postgresql:postgresql:42.6.0

=== Ringkasan perbandingan ===
commonMain : 6 artifacts
androidMain: 9 artifacts (common + platform)
iosMain    : 7 artifacts (common + platform)
jvmMain    : 9 artifacts (common + platform)

=== Source set spesifik (platform-only) ===
androidMain khusus: [androidx.lifecycle:lifecycle-viewmodel:2.6.2, com.squareup.okhttp3:okhttp:4.12.0, io.ktor:ktor-client-okhttp:2.3.7]
iosMain khusus    : [io.ktor:ktor-client-darwin:2.3.7]
jvmMain khusus    : [com.squareup.okhttp3:okhttp:4.12.0, io.ktor:ktor-client-okhttp:2.3.7, org.postgresql:postgresql:42.6.0]

Mari kita bedah bagian kunci dari kode di atas:

  1. data class Artifact: model untuk satu library—group:name:version, daftar target yang kompatibel, dan daftar dependency transitif. notation() mengembalikan string Maven standar.
  2. put(Artifact(...)): populasi katalog dengan 11 library. kotlinx-serialization-json memuat kotlinx-serialization-core secara transitif—sama seperti di dunia nyata.
  3. DependencyResolver.resolve(sourceSet, deps): BFS sederhana yang menelusuri dependency transitif. Bila library A membutuhkan B, B ditambahkan ke queue. Hasilnya adalah himpunan lengkap artifact di classpath.
  4. Empat daftar input: commonDeps untuk commonMain, lalu androidDeps, iosDeps, jvmDeps ditambah commonDeps karena platform source set mewarisi commonMain.
  5. Output commonMain: 6 artifact—5 library KMP plus ktor-client-core yang ditarik transitif dari ktor-client-content-negotiation.
  6. Output androidMain: 9 artifact—6 dari commonMain plus okhttp, ktor-client-okhttp, dan lifecycle-viewmodel.
  7. Output iosMain: 7 artifact—6 dari commonMain plus ktor-client-darwin. Tidak ada okhttp karena iOS tidak memakainya.
  8. Output jvmMain: 9 artifact—sama dengan androidMain kecuali lifecycle-viewmodel diganti postgresql. Inilah inti pemisahan platform source set.

Aturan Memilih Dependency commonMain #

Library yang ditambahkan ke commonMain harus menyatakan dukungan KMP secara eksplisit di dokumentasi. Tiga ciri library KMP: artifact di Maven Central memiliki classifier -jvm, -android, -iosarm64, -linuxx64; build.gradle.kts library memakai plugin kotlin("multiplatform"); dan README menyebutkan dukungan KMP. Library tanpa ciri ini hanya boleh ditambahkan ke source set spesifik.

Referensi Lanjutan #

Penutup #

Manajemen dependency KMP adalah kelanjutan langsung dari pola source set—library commonMain harus kompatibel KMP; library platform-specific ditambahkan ke source set spesifik. Resolusi transitif dikelola otomatis oleh Gradle, tetapi pemahaman struktur classpath per source set penting untuk debugging error kompilasi. Chapter berikutnya akan masuk ke pengujian KMP: bagaimana commonTest memungkinkan kita menulis test sekali yang dijalankan di setiap target.