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.

AspekcommonMainandroidMain / iosMain / jvmMain
Kompilasi targetSemua targetSatu target spesifik
Boleh pakai API platformTidakYa (java.io, UIKit, android.util)
Visibilitas kodeTerlihat di semua source setHanya source set itu sendiri
Boleh ada expectYaTidak (cuma actual)
Library KMPBoleh semuaBoleh 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.

kotlin
// 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:

text
=== 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: 1

Mari kita bedah bagian kunci dari kode di atas:

  1. interface LogPlatform: abstraksi di commonMain—deklarasi kontrak tanpa implementasi. Kode bisnis Logger dan ManajerSesi hanya bergantung pada interface ini, bukan pada API platform mana pun.
  2. class Logger: kode bisnis murni—memformat pesan dan mendelegasikan penulisan ke LogPlatform. Class ini akan berada di commonMain dan dapat dipanggil dari semua source set.
  3. class ManajerSesi: mengelola state Sesi dengan MutableMap—kode murni Kotlin tanpa API platform. Inilah kode yang ingin kita bagikan lintas platform.
  4. AndroidLog, IosLog, JvmLog: tiga implementasi LogPlatform. Pada proyek KMP nyata, ketiganya berada di source set terpisah (androidMain, iosMain, jvmMain) dan dihubungkan ke commonMain via expect/actual.
  5. PlatformProvider: factory yang menyembunyikan pemilihan implementasi dari main(). Pola ini umum dipakai untuk injeksi dependency—kode pemanggil tidak peduli platform mana yang aktif.
  6. 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 commonMain dan *Main per 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.