Setelah memahami hierarki source set pada chapter sebelumnya, kita perlu memahami bagaimana kode benar-benar diorganisir antara commonMain dan platform source sets. commonMain berisi kode yang ditulis sekali dan dikompilasi untuk semua target—mulai dari model data hingga aturan bisnis. Source set seperti jvmMain, androidMain, dan iosMain berisi kode yang memanggil API spesifik platform. Komunikasi antara keduanya tidak bebas: commonMain tidak boleh langsung memanggil API platform. Pada chapter ini kita akan membedah kontrak antara commonMain dan platform source sets, memahami kapan sebuah kode harus pindah ke source set spesifik, dan melihat mekanisme expect/actual yang menjadi penghubung resmi.
Peran commonMain #
commonMain adalah satu-satunya source set yang dapat dipanggil oleh semua target. Kode yang berada di sini hanya boleh memakai kotlin-stdlib dan library yang mendukung KMP—tidak boleh langsung memanggil java.io.File, android.util.Log, atau UIKit. Aturan ketat ini memungkinkan kode commonMain dikompilasi ke bytecode JVM, JavaScript, dan native binary tanpa modifikasi.
Pada praktik nyata, commonMain berisi: model data (data class), aturan validasi, logika bisnis, definisi interface untuk fungsi platform-specific, dan deklarasi expect yang menunggu actual di setiap source set spesifik. Pemisahan ini membuat kode bisnis tetap bersih dan dapat diuji tanpa menyalakan emulator atau device.
Hubungan commonMain dan Platform Source Sets #
Setiap source set spesifik (androidMain, iosMain, jvmMain) mewarisi commonMain melalui hubungan dependsOn. Artinya, kode di commonMain dapat dipanggil dari androidMain tanpa import tambahan. Sebaliknya, kode di androidMain tidak dapat dipanggil dari commonMain atau jvmMain—kompiler menolak pemanggilan karena kode platform tidak terlihat dari commonMain.
| Aspek | commonMain | androidMain / iosMain / jvmMain |
|---|---|---|
| Kompilasi target | Semua target | Satu target spesifik |
| Boleh pakai API platform | Tidak | Ya (java.io, UIKit, android.util) |
| Visibilitas kode | Terlihat di semua source set | Hanya source set itu sendiri |
Boleh ada expect | Ya | Tidak (cuma actual) |
| Library KMP | Boleh semua | Boleh library KMP + library platform-only |
Hubungan dependsOn juga berlaku untuk intermediate source set. iosMain bergantung pada commonMain, lalu iosX64Main bergantung pada iosMain. Kode di commonMain dapat dipanggil dari iosX64Main; kode di iosMain juga dapat dipanggil dari iosX64Main. Sebaliknya, kode di iosX64Main tidak dapat dipanggil dari commonMain.
Simulasi Pola commonMain dan Platform Source Sets #
Karena tutorial ini berjalan di JVM, kita tidak dapat menjalankan kode KMP nyata. Demo berikut mensimulasikan pola tersebut dengan interface dan tiga implementasi, mirip dengan pola yang dipakai KMP pada proyek nyata.
// commonmain_dan_platform_source_sets.kt
// commonMain: shared code. Platform source sets: jvmMain, androidMain, iosMain.
// expect/actual mechanism (simulated via interface for runnable demo).
// --- commonMain: pure Kotlin, no platform APIs ---
interface LogPlatform {
fun tag(): String
fun write(level: String, message: String)
}
// Pure Kotlin domain code in commonMain
class Logger(private val platform: LogPlatform) {
fun info(message: String) = platform.write("INFO", message)
fun error(message: String) = platform.write("ERROR", message)
fun infoWithTag(message: String) {
platform.write("INFO", "[${platform.tag()}] $message")
}
}
data class Sesi(val id: String, val pengguna: String, val aktif: Boolean)
class ManajerSesi(private val logger: Logger) {
private val sesi = mutableMapOf<String, Sesi>()
fun buat(pengguna: String): Sesi {
val s = Sesi(id = "SES-${sesi.size + 1}", pengguna = pengguna, aktif = true)
sesi[s.id] = s
logger.infoWithTag("Sesi dibuat untuk $pengguna")
return s
}
fun tutup(id: String): Boolean {
val s = sesi[id] ?: run {
logger.error("Sesi $id tidak ditemukan")
return false
}
sesi[id] = s.copy(aktif = false)
logger.infoWithTag("Sesi $id ditutup")
return true
}
fun jumlahAktif(): Int = sesi.values.count { it.aktif }
}
// --- Platform-specific implementations (would live in respective source sets) ---
// androidMain
class AndroidLog : LogPlatform {
override fun tag() = "AppTag"
override fun write(level: String, message: String) {
// Di Android nyata: android.util.Log.println(level, tag(), message)
println("[Android/$level/${tag()}] $message")
}
}
// iosMain
class IosLog : LogPlatform {
override fun tag() = "AppTag"
override fun write(level: String, message: String) {
// Di iOS nyata: os_log transmit to system logger
println("[iOS/$level/${tag()}] $message")
}
}
// jvmMain
class JvmLog : LogPlatform {
override fun tag() = "AppTag"
override fun write(level: String, message: String) {
// Di JVM nyata: java.util.logging.Logger
println("[JVM/$level/${tag()}] $message")
}
}
// --- Simulate expect/actual mechanism ---
// In real KMP:
// commonMain: expect fun platformName(): String
// androidMain: actual fun platformName(): String = "Android"
// iosMain: actual fun platformName(): String = "iOS"
//
// Here we simulate with a top-level provider interface.
interface PlatformProvider {
fun name(): String
fun createLog(): LogPlatform
}
object AndroidProvider : PlatformProvider {
override fun name() = "Android"
override fun createLog() = AndroidLog()
}
object IosProvider : PlatformProvider {
override fun name() = "iOS"
override fun createLog() = IosLog()
}
object JvmProvider : PlatformProvider {
override fun name() = "JVM"
override fun createLog() = JvmLog()
}
fun main() {
for (provider in listOf(AndroidProvider, IosProvider, JvmProvider)) {
println("=== Platform: ${provider.name()} ===")
val logger = Logger(provider.createLog())
val manajer = ManajerSesi(logger)
manajer.buat("budi")
manajer.buat("siti")
manajer.tutup("SES-1")
manajer.tutup("SES-99")
println("Sesi aktif: ${manajer.jumlahAktif()}")
println()
}
}Output yang Dihasilkan:
=== Platform: Android ===
[Android/INFO/AppTag] [AppTag] Sesi dibuat untuk budi
[Android/INFO/AppTag] [AppTag] Sesi dibuat untuk siti
[Android/INFO/AppTag] [AppTag] Sesi SES-1 ditutup
[Android/ERROR/AppTag] Sesi SES-99 tidak ditemukan
Sesi aktif: 1
=== Platform: iOS ===
[iOS/INFO/AppTag] [AppTag] Sesi dibuat untuk budi
[iOS/INFO/AppTag] [AppTag] Sesi dibuat untuk siti
[iOS/INFO/AppTag] [AppTag] Sesi SES-1 ditutup
[iOS/ERROR/AppTag] Sesi SES-99 tidak ditemukan
Sesi aktif: 1
=== Platform: JVM ===
[JVM/INFO/AppTag] [AppTag] Sesi dibuat untuk budi
[JVM/INFO/AppTag] [AppTag] Sesi dibuat untuk siti
[JVM/INFO/AppTag] [AppTag] Sesi SES-1 ditutup
[JVM/ERROR/AppTag] Sesi SES-99 tidak ditemukan
Sesi aktif: 1Mari kita bedah bagian kunci dari kode di atas:
interface LogPlatform: abstraksi dicommonMain—deklarasi kontrak tanpa implementasi. Kode bisnisLoggerdanManajerSesihanya bergantung pada interface ini, bukan pada API platform mana pun.class Logger: kode bisnis murni—memformat pesan dan mendelegasikan penulisan keLogPlatform. Class ini akan berada dicommonMaindan dapat dipanggil dari semua source set.class ManajerSesi: mengelola stateSesidenganMutableMap—kode murni Kotlin tanpa API platform. Inilah kode yang ingin kita bagikan lintas platform.AndroidLog,IosLog,JvmLog: tiga implementasiLogPlatform. Pada proyek KMP nyata, ketiganya berada di source set terpisah (androidMain,iosMain,jvmMain) dan dihubungkan kecommonMainviaexpect/actual.PlatformProvider: factory yang menyembunyikan pemilihan implementasi darimain(). Pola ini umum dipakai untuk injeksi dependency—kode pemanggil tidak peduli platform mana yang aktif.- Output menunjukkan tiga run: kode bisnis yang sama (
Logger,ManajerSesi) dijalankan dengan tiga implementasi platform berbeda—hasil logika identik, hanya header platform yang berbeda.
Aturan Memindahkan Kode ke Platform Source Set #
Kapan sebuah kode harus pindah dari commonMain ke source set spesifik? Aturan utamanya: bila kode membutuhkan API platform, ia harus pindah. Misalnya, akses filesystem di JVM memakai java.io.File, di iOS memakai NSFileManager, di Android memakai Context.getFilesDir(). Ketiganya tidak dapat ditulis di commonMain.
Solusi yang dipakai: deklarasikan expect fun bacaFile(nama: String): String di commonMain. Lalu tulis actual fun bacaFile(nama: String): String di jvmMain dengan java.io.File, di iosMain dengan NSFileManager, di androidMain dengan Context. Kode bisnis di commonMain cukup memanggil bacaFile("config.json")—kompilator akan memilih actual yang sesuai untuk setiap target.
Referensi Lanjutan #
- Discover KMP project
— panduan source set
commonMaindan*Mainper platform di proyek KMP - Hierarchy template — dokumentasi hierarchy template untuk berbagi kode antar source set intermediate
Penutup #
Pemisahan commonMain dan platform source sets adalah inti dari pola KMP. commonMain menyimpan kode bisnis yang dibagi; platform source sets menyimpan kode yang memanggil API platform. Komunikasi antara keduanya dijembatani oleh mekanisme expect/actual yang akan kita bedah tuntas pada chapter berikutnya.