Domain logic di Bagian 1 dan data layer di Bagian 2 dapat diabstraksi via interface. Namun beberapa fitur tidak dapat diabstraksi secara elegan: bagaimana mendapatkan waktu saat ini? Bagaimana cara logging yang pakai Log di Android dan NSLog di iOS? Bagaimana path file storage yang berbeda (/tmp di Linux, NSTemporaryDirectory() di iOS, context.filesDir di Android)? Untuk kasus ini, KMP menyediakan expect/actual: deklarasi di commonMain (expect), implementasi di source set platform (actual). Pada Bagian 3 ini kita membangun Platform abstraction via expect/actual untuk tiga fitur: nama platform, waktu saat ini, dan path file storage.

Bagaimana expect/actual Bekerja #

Di commonMain, kita deklarasikan signature tanpa body:

kotlin expect di commonMain
expect fun platformName(): String
expect fun currentTimeIso(): String
expect fun loggerTag(): String
expect fun pathForFile(fileName: String): String

Di setiap source set platform, kita berikan implementasi aktual:

kotlin actual di jvmMain
actual fun platformName(): String = "JVM (OpenJDK 21)"
actual fun currentTimeIso(): String = LocalDateTime.now().toString()
actual fun loggerTag(): String = "JVM-APP"
actual fun pathForFile(fileName: String): String =
    Paths.get(System.getProperty("user.dir"), fileName).toString()

Kompilator memverifikasi bahwa setiap expect di commonMain memiliki actual di setiap target platform. Jika lupa, kompilasi gagal dengan pesan yang jelas.

Implementasi Platform per Target #

Kode berikut mendemonstrasikan tiga implementasi Platform (JVM, Android, iOS) dengan tiga fitur yang berbeda implementasinya. Karena kode contoh berjalan di terminal JVM, kita mengekspresikan pola via interface + object per platform; di KMP asli, kode yang sama akan dipisah ke source set platform:

kotlin
// Bagian 3: Platform-specific integration via expect/actual.
// Mendemonstrasikan pola expect/actual KMP dengan interface + impl
// per platform. Logger, waktu saat ini, dan persistence path berbeda
// per platform tapi kontraknya sama.

// === commonMain: deklarasi expect (interface di sini untuk demo terminal) ===
// Pada KMP asli, ditulis sebagai:
//   expect fun platformName(): String
//   expect fun currentTimeIso(): String
//   expect fun loggerTag(): String

interface Platform {
    fun name(): String
    fun currentTimeIso(): String
    fun loggerTag(): String
    fun pathForFile(fileName: String): String
}

// === jvmMain: actual ===
object JvmPlatform : Platform {
    override fun name(): String = "JVM (OpenJDK 21)"
    override fun currentTimeIso(): String {
        // Di KMP asli, java.time tersedia di jvmMain saja.
        val now = java.time.LocalDateTime.of(2026, 1, 15, 10, 30, 0)
        return now.toString()
    }
    override fun loggerTag(): String = "JVM-APP"
    override fun pathForFile(fileName: String): String =
        java.nio.file.Paths.get(System.getProperty("user.dir"), fileName).toString()
}

// === androidMain: actual (disimulasikan) ===
object AndroidPlatform : Platform {
    override fun name(): String = "Android (API 34)"
    override fun currentTimeIso(): String {
        // Di Android, pakai java.util.Calendar atau java.time (API 26+)
        val cal = java.util.GregorianCalendar(2026, 0, 15, 10, 30, 0)
        val tahun = cal.get(java.util.Calendar.YEAR)
        val bulan = (cal.get(java.util.Calendar.MONTH) + 1).toString().padStart(2, '0')
        val hari = cal.get(java.util.Calendar.DAY_OF_MONTH).toString().padStart(2, '0')
        val jam = cal.get(java.util.Calendar.HOUR_OF_DAY).toString().padStart(2, '0')
        val menit = cal.get(java.util.Calendar.MINUTE).toString().padStart(2, '0')
        return "$tahun-$bulan-${hari}T$jam:$menit:00"
    }
    override fun loggerTag(): String = "Android-App"
    override fun pathForFile(fileName: String): String =
        // Di Android asli: context.filesDir.path + "/" + fileName
        "/data/data/com.example.app/files/$fileName"
}

// === iosMain: actual (disimulasikan) ===
object IosPlatform : Platform {
    override fun name(): String = "iOS (Kotlin/Native)"
    override fun currentTimeIso(): String {
        // Di iOS asli: pakai kotlinx-datetime atau NSDate
        return "2026-01-15T10:30:00"
    }
    override fun loggerTag(): String = "iOS-App"
    override fun pathForFile(fileName: String): String =
        // Di iOS asli: NSTemporaryDirectory() + fileName
        "/tmp/$fileName"
}

// === commonMain: shared code yang memakai Platform ===
class PlatformService(private val platform: Platform) {
    fun info(): String {
        val sb = StringBuilder()
        sb.appendLine("Platform: ${platform.name()}")
        sb.appendLine("Waktu   : ${platform.currentTimeIso()}")
        sb.appendLine("Tag     : ${platform.loggerTag()}")
        sb.appendLine("Storage : ${platform.pathForFile("app.db")}")
        return sb.toString().trimEnd()
    }
}

// === commonMain: factory function (expect fun buatPlatform(): Platform) ===
// Pada KMP asli:
//   expect fun buatPlatform(): Platform
//
// Di setiap source set platform:
//   actual fun buatPlatform(): Platform = JvmPlatform
//
// Di sini, kita simulasikan dengan list eksplisit karena runtime JVM.
fun main() {
    val platforms = listOf(
        "JVM" to JvmPlatform,
        "Android" to AndroidPlatform,
        "iOS" to IosPlatform
    )

    platforms.forEach { (label, platform) ->
        println("=== $label ===")
        val service = PlatformService(platform)
        println(service.info())
        println()
    }

    println("=== Penggunaan di aplikasi (gaya expect/actual) ===")
    // Pada aplikasi nyata, platform dipilih otomatis oleh kompilator
    // berdasarkan target. Kode commonMain tidak peduli platform mana.
    val appPlatform: Platform = JvmPlatform  // "actual" yang dipilih compiler
    val service = PlatformService(appPlatform)
    println(service.info())
}

Output yang Dihasilkan:

text
=== JVM ===
Platform: JVM (OpenJDK 21)
Waktu   : 2026-01-15T10:30
Tag     : JVM-APP
Storage : /home/z/my-project/tutorial/app.db

=== Android ===
Platform: Android (API 34)
Waktu   : 2026-01-15T10:30:00
Tag     : Android-App
Storage : /data/data/com.example.app/files/app.db

=== iOS ===
Platform: iOS (Kotlin/Native)
Waktu   : 2026-01-15T10:30:00
Tag     : iOS-App
Storage : /tmp/app.db

=== Penggunaan di aplikasi (gaya expect/actual) ===
Platform: JVM (OpenJDK 21)
Waktu   : 2026-01-15T10:30
Tag     : JVM-APP
Storage : /home/z/my-project/tutorial/app.db

Mari kita bedah komponen utama:

  1. interface Platform di commonMain (simulasi expect fun): kontrak dengan empat method. Di KMP asli, diganti dengan expect fun name(): String dan seterusnya; kompilator menghasilkan function yang dapat dipanggil di commonMain tanpa peduli platform.
  2. object JvmPlatform : Platform (simulasi actual fun di jvmMain): implementasi yang memakai java.time.LocalDateTime (JVM-only API). Di KMP asli, kode ini hanya di-compile untuk target JVM; compiler tidak memeriksa kompatibilitas Android/iOS.
  3. AndroidPlatform memakai java.util.Calendar: di Android, java.time hanya tersedia di API 26+; untuk peranti lama, pakai Calendar. actual di androidMain boleh memakai library Android SDK (android.util.Log, Context, dst.) yang tidak tersedia di commonMain.
  4. IosPlatform mengembalikan string beku: karena kode demo berjalan di JVM, kita tidak benar-benar memanggil NSDate. Di KMP asli, iosMain dapat memakai kotlinx.cinterop untuk memanggil Objective-C API. NSTemporaryDirectory() akan menjadi implementasi actual fun pathForFile.
  5. PlatformService(platform) di commonMain: kode yang memakai Platform. Tidak tahu implementasi mana yang dipakai. Pola ini memungkinkan kode di commonMain tetap murni tanpa cabang if (platform == "JVM").

expect untuk Property dan Class #

expect tidak terbatas pada function. Bisa juga untuk property dan class:

kotlin expect untuk property dan class
// commonMain
expect val defaultLocale: String
expect class PlatformFile(path: String) {
    fun read(): String
    fun write(content: String)
}

// jvmMain
actual val defaultLocale: String = System.getProperty("user.language")
actual class PlatformFile actual constructor(path: String) {
    private val file = java.io.File(path)
    actual fun read(): String = file.readText()
    actual fun write(content: String) = file.writeText(content)
}

Pola expect class berguna saat kita ingin class di commonMain yang implementasinya benar-benar berbeda per platform (misalnya, file I/O via java.io.File di JVM, NSFileManager di iOS).

expect/actual vs Interface #

Kapan memilih expect/actual dan kapan interface? Aturan umum:

  • Interface: jika implementasi dapat di-pass sebagai dependency (DI), dan ada banyak implementasi yang aktif dalam runtime yang sama. Cocok untuk repository, service, plugin.
  • expect/actual: jika hanya ada satu implementasi per platform (logger, sistem waktu, file system), dan tidak perlu switch di runtime. Implementasi dipilih di compile time, bukan runtime.

Kode di Bagian 2 (TransaksiRepo) memakai interface karena ada banyak impl yang dapat dipilih saat runtime. Kode di Bagian 3 (Platform) memakai expect/actual karena hanya ada satu platform per kompilasi.

Referensi Lanjutan #

Ringkasan Bagian 3 #

Platform-specific integration kini lengkap via expect/actual. Tiga fitur yang berbeda per platform (nama platform, waktu saat ini, path file storage) dideklarasikan di commonMain dan diimplementasi di jvmMain, androidMain, iosMain. Use case PlatformService tetap di commonMain dan tidak peduli platform. Pada Bagian 4 kita menulis pengujian di commonTest dan membangun library untuk semua target platform.