Proyek Kotlin biasa memiliki satu source set main yang dikompilasi ke satu target (biasanya JVM). Proyek KMP berbeda: satu proyek mendeklarasikan beberapa target platform, dan kompilator Kotlin mengelola source set per target plus source set bersama commonMain. Memahami hubungan antar source set—mana yang berbagi kode, mana yang spesifik platform—adalah pondasi yang harus dikuasai sebelum menulis kode KMP nyata. Pada chapter ini kita akan membedah deklarasi target di build.gradle.kts, hierarki source set, dan bagaimana satu modul commonMain dapat dipakai oleh lima target sekaligus.

Deklarasi Target di build.gradle.kts #

Blok kotlin { ... } dalam build.gradle.kts mendeklarasikan target platform yang didukung proyek. Setiap target menghasilkan artifact berbeda—androidTarget() menghasilkan AAR/APK, iosX64() dan iosArm64() menghasilkan framework iOS, jvm() menghasilkan JAR. Pemilihan target menentukan source set yang dibuat secara otomatis.

kotlin build.gradle.kts: deklarasi target
plugins {
    kotlin("multiplatform") version "2.0.21"
}

kotlin {
    androidTarget()
    iosX64()
    iosArm64()
    iosSimulatorArm64()
    jvm()
}

Empat target iOS umumnya ditambahkan bersamaan—tiga untuk arsitektur yang berbeda (x64 simulator Intel, ARM64 device, ARM64 simulator Apple Silicon). Kompilator Kotlin akan menghasilkan framework per arsitektur, kemudian Xcode melakukan fat framework atau XCFramework untuk menggabungkannya.

Hierarki Source Set #

Source set adalah direktori yang berisi kode Kotlin. KMP membangun hierarki source set agar kode dapat berbagi pada level yang sesuai. commonMain adalah root—kode di sini dipakai semua target. iosMain adalah intermediate—dipakai semua target iOS tetapi tidak oleh Android atau JVM. iosX64Main spesifik untuk iOS x64 saja.

kotlin
// multiplatform_project_structure.kt
// build.gradle.kts with kotlin { androidTarget(); iosX64(); iosArm64() }.
// Source sets: commonMain, jvmMain, nativeMain.
// This demo simulates the source-set hierarchy as data structures.

data class SourceSet(
    val name: String,
    val dependsOn: List<String>,
    val description: String
)

data class Target(
    val preset: String,    // androidTarget, iosX64, iosArm64, iosSimulatorArm64, jvm
    val platform: String,
    val outputType: String // APK, framework, JAR
)

data class ProjectLayout(
    val targets: List<Target>,
    val sourceSets: List<SourceSet>
)

// --- KMP project layout for Android + iOS + JVM ---
fun sampleKmpLayout(): ProjectLayout {
    val targets = listOf(
        Target("androidTarget()", "Android", "AAR / APK"),
        Target("iosX64()",        "iOS x64 (simulator Intel)", "framework"),
        Target("iosArm64()",      "iOS ARM64 (device)",         "framework"),
        Target("iosSimulatorArm64()", "iOS ARM64 (simulator M1/M2)", "framework"),
        Target("jvm()",           "JVM",                        "JAR")
    )
    val sourceSets = listOf(
        SourceSet("commonMain", emptyList(),
            "Kode yang dipakai di semua target; pure Kotlin"),
        SourceSet("androidMain", listOf("commonMain"),
            "Implementasi Android: android.util.Log, Context, SharedPreferences"),
        SourceSet("iosMain", listOf("commonMain"),
            "Kode intermediate yang dipakai semua target iOS"),
        SourceSet("iosX64Main", listOf("iosMain"),
            "Spesifik iOS x64 (simulator Intel)"),
        SourceSet("iosArm64Main", listOf("iosMain"),
            "Spesifik iOS ARM64 (device)"),
        SourceSet("iosSimulatorArm64Main", listOf("iosMain"),
            "Spesifik iOS ARM64 (simulator M1/M2)"),
        SourceSet("jvmMain", listOf("commonMain"),
            "Implementasi JVM: java.io.File, java.net.URL, JDBC")
    )
    return ProjectLayout(targets, sourceSets)
}

// --- Print build.gradle.kts equivalent (illustrative) ---
fun buildGradleKts(): String = """
plugins {
    kotlin("multiplatform") version "2.0.21"
}

kotlin {
    androidTarget()
    iosX64()
    iosArm64()
    iosSimulatorArm64()
    jvm()

    sourceSets {
        val commonMain by getting {
            dependencies {
                implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
                implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0")
            }
        }
        val androidMain by getting
        val iosMain by creating {
            dependsOn(commonMain)
        }
        val iosX64Main by getting { dependsOn(iosMain) }
        val iosArm64Main by getting { dependsOn(iosMain) }
        val iosSimulatorArm64Main by getting { dependsOn(iosMain) }
        val jvmMain by getting
    }
}
""".trim()

fun main() {
    val layout = sampleKmpLayout()

    println("=== Target Platform ===")
    for (t in layout.targets) {
        println("  ${t.preset.padEnd(28)} -> ${t.platform.padEnd(28)} output: ${t.outputType}")
    }
    println()

    println("=== Source Sets ===")
    for (ss in layout.sourceSets) {
        val deps = if (ss.dependsOn.isEmpty()) "(top-level)" else ss.dependsOn.joinToString(", ")
        println("  ${ss.name.padEnd(28)} depends-on: $deps")
        println("    ${ss.description}")
    }
    println()

    println("=== build.gradle.kts (ringkas) ===")
    println(buildGradleKts())
}

Output yang Dihasilkan:

text
=== Target Platform ===
  androidTarget()              -> Android                      output: AAR / APK
  iosX64()                     -> iOS x64 (simulator Intel)    output: framework
  iosArm64()                   -> iOS ARM64 (device)           output: framework
  iosSimulatorArm64()          -> iOS ARM64 (simulator M1/M2)  output: framework
  jvm()                        -> JVM                          output: JAR

=== Source Sets ===
  commonMain                   depends-on: (top-level)
    Kode yang dipakai di semua target; pure Kotlin
  androidMain                  depends-on: commonMain
    Implementasi Android: android.util.Log, Context, SharedPreferences
  iosMain                      depends-on: commonMain
    Kode intermediate yang dipakai semua target iOS
  iosX64Main                   depends-on: iosMain
    Spesifik iOS x64 (simulator Intel)
  iosArm64Main                 depends-on: iosMain
    Spesifik iOS ARM64 (device)
  iosSimulatorArm64Main        depends-on: iosMain
    Spesifik iOS ARM64 (simulator M1/M2)
  jvmMain                      depends-on: commonMain
    Implementasi JVM: java.io.File, java.net.URL, JDBC

=== build.gradle.kts (ringkas) ===
plugins {
    kotlin("multiplatform") version "2.0.21"
}

kotlin {
    androidTarget()
    iosX64()
    iosArm64()
    iosSimulatorArm64()
    jvm()

    sourceSets {
        val commonMain by getting {
            dependencies {
                implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
                implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0")
            }
        }
        val androidMain by getting
        val iosMain by creating {
            dependsOn(commonMain)
        }
        val iosX64Main by getting { dependsOn(iosMain) }
        val iosArm64Main by getting { dependsOn(iosMain) }
        val iosSimulatorArm64Main by getting { dependsOn(iosMain) }
        val jvmMain by getting
    }
}

Mari kita bedah bagian kunci dari kode di atas:

  1. SourceSet(name, dependsOn, description): model data yang menggambarkan satu source set. dependsOn adalah daftar source set yang menjadi parent—androidMain bergantung pada commonMain, sehingga kode di commonMain dapat dipanggil dari androidMain.
  2. Target(preset, platform, outputType): model data untuk satu target platform. preset adalah nama fungsi yang dipanggil di blok kotlin {} (mis. androidTarget()); outputType adalah jenis artifact yang dihasilkan.
  3. sampleKmpLayout: membangun layout lengkap untuk proyek KMP dengan 5 target. iosMain diciptakan sebagai intermediate source set—kode di sini dipakai oleh iosX64Main, iosArm64Main, dan iosSimulatorArm64Main.
  4. buildGradleKts: string template yang merepresentasikan build.gradle.kts ringkas. Blok sourceSets mendeklarasikan hubungan dependsOn secara eksplisit—iosMain bergantung pada commonMain, dan ketiga source set iOS spesifik bergantung pada iosMain.
  5. Output menunjukkan hierarki lengkap: commonMain adalah top-level, androidMain/iosMain/jvmMain adalah turunan langsung, dan iosX64Main/iosArm64Main/iosSimulatorArm64Main adalah turunan dari iosMain. Kode di commonMain dapat dipanggil dari semua source set; kode di iosMain hanya dari source set iOS.

Struktur Direktori #

Pada filesystem, source set direpresentasikan sebagai direktori src/<source-set>/kotlin/. Struktur tipikal proyek KMP:

text
my-app/
├── build.gradle.kts
├── settings.gradle.kts
└── src/
    ├── commonMain/
    │   └── kotlin/
    │       └── com/example/shared/
    │           └── LogikaBisnis.kt
    ├── androidMain/
    │   └── kotlin/
    │       └── com/example/shared/
    │           └── Platform.android.kt
    ├── iosMain/
    │   └── kotlin/
    │       └── com/example/shared/
    │           └── Platform.ios.kt
    └── jvmMain/
        └── kotlin/
            └── com/example/shared/
                └── Platform.jvm.kt

Konvensi nama file dengan sufiks .android.kt, .ios.kt, .jvm.kt memudahkan navigasi—semua file untuk class yang sama (Platform) berdampingan di editor. Paket (com.example.shared) harus sama di semua source set agar kompilator dapat mencocokkan deklarasi expect dengan actual.

Source Set Intermediate #

iosMain adalah contoh intermediate source set—tidak ada target langsung yang menggunakannya, namun ketiga source set iOS spesifik (iosX64Main, iosArm64Main, iosSimulatorArm64Main) mewarisinya. Intermediate source set berguna bila kode iOS spesifik (mis. pemanggilan UIKit) ingin dibagi antara ketiga target iOS tanpa menulis ulang.

kotlin Membuat intermediate source set
kotlin {
    iosX64()
    iosArm64()
    iosSimulatorArm64()

    sourceSets {
        val iosMain by creating {
            dependsOn(commonMain)
        }
        val iosX64Main by getting { dependsOn(iosMain) }
        val iosArm64Main by getting { dependsOn(iosMain) }
        val iosSimulatorArm64Main by getting { dependsOn(iosMain) }
    }
}

Pada Kotlin 2.0+, hierarki source set untuk iOS dibuat otomatis oleh plugin—iosMain tersedia tanpa deklarasi manual. Versi sebelumnya memerlukan deklarasi creating seperti di atas. Tetap periksa dokumentasi terbaru karena konvensi berubah antar versi.

Dependency per Source Set #

Setiap source set dapat memiliki dependency sendiri. commonMain memakai library yang mendukung KMP (mis. kotlinx-coroutines-core); androidMain dapat menambah library Android-specific (mis. androidx.lifecycle); jvmMain dapat menambah library JVM-only (mis. postgresql).

kotlin Dependency per source set
sourceSets {
    val commonMain by getting {
        dependencies {
            implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
        }
    }
    val androidMain by getting {
        dependencies {
            implementation("androidx.lifecycle:lifecycle-viewmodel:2.6.2")
        }
    }
    val jvmMain by getting {
        dependencies {
            implementation("org.postgresql:postgresql:42.6.0")
        }
    }
}

Library di commonMain otomatis tersedia di semua source set turunan. Library di androidMain hanya tersedia di androidMain dan source set turunannya—tidak bocor ke jvmMain.

Referensi Lanjutan #

Penutup #

Struktur proyek KMP—deklarasi target, hierarki source set, dan dependency per source set—adalah konsep yang terasa asing bagi pengembang yang terbiasa dengan proyek single-platform. Setelah pola ini dipahami, KMP memberikan keuntungan besar: kode bisnis ditulis sekali, dan setiap platform mendapatkan artifact yang sesuai. Chapter berikutnya akan masuk lebih dalam ke commonMain dan platform source sets—bagaimana kode diorganisir di antara keduanya.