Tidak semua kode dapat berbagi lintas platform. Akses filesystem, panggilan jaringan, thread pool, dan logging memiliki API yang berbeda total antara JVM, Android, dan iOS. Tantangannya bukan menulis kode platform-specific—itu mudah—melainkan menentukan kapan sebuah fungsi harus pindah ke source set spesifik, dan bagaimana menjaga kode bisnis tetap bersih. Pada chapter ini kita akan melihat empat kategori umum kode platform-specific, pola injeksi interface yang menjadi alternatif expect/actual, dan demonstrasi tiga implementasi platform yang berbeda melayani satu logika bisnis tunggal.
Empat Kategori Kode Platform-Specific #
Kode yang biasanya perlu dipindah ke source set spesifik dapat dikelompokkan menjadi empat kategori. File I/O misalnya—JVM memakai java.io.File, Android memakai Context.openFileOutput, iOS memakai NSFileManager. Network—JVM memakai HttpURLConnection atau OkHttp, Android memakai OkHttp, iOS memakai NSURLSession. Threading—JVM memakai ExecutorService, Android memakai Handler atau Dispatchers.Main, iOS memakai DispatchQueue.main. Logging—JVM memakai java.util.logging, Android memakai android.util.Log, iOS memakai os_log.
| Kategori | JVM | Android | iOS |
|---|---|---|---|
| File I/O | java.io.File, Path | Context.openFileOutput | NSFileManager |
| Network | HttpURLConnection, OkHttp | OkHttp, Volley | NSURLSession, Darwin |
| Threading | ExecutorService | Dispatchers.Main, Handler | DispatchQueue.main |
| Logging | java.util.logging | android.util.Log | os_log |
Empat kategori ini tidak dapat dipakai langsung di commonMain. Solusinya: deklarasikan interface atau expect di commonMain, lalu berikan implementasi per platform.
Pola Injeksi Interface sebagai Alternatif expect/actual #
expect/actual cocok untuk fungsi kecil yang berdiri sendiri. Untuk komponen yang lebih kompleks—misalnya penyimpanan yang dipakai banyak class—pola injeksi interface sering lebih fleksibel. Kita mendefinisikan interface di commonMain, lalu menyuntikkan implementasi platform-specific melalui constructor.
Keuntungan pola ini: mudah diuji karena implementasi bisa ditukar dengan mock; tidak bergantung pada keyword expect/actual yang hanya tersedia di plugin KMP; dan dapat dipakai di proyek non-KMP. Kerugian: boilerplate sedikit lebih banyak—kita perlu menyuntikkan dependency ke setiap class yang membutuhkannya.
Simulasi Implementasi Platform #
Karena tutorial ini berjalan di JVM, kita mensimulasikan tiga platform dengan tiga class implementasi. Pola yang ditampilkan—Penyimpanan, PemutusLog, PabrikUuid sebagai interface, dengan tiga implementasi—adalah pola yang sama dipakai di proyek KMP nyata.
// platform_specific_code.kt
// Kapan menulis kode platform-specific: file I/O, network, threading, logging.
// Demo: logika umum di commonMain, implementasi platform via interface
// (sama seperti expect/actual tetapi memakai dependency injection).
// --- commonMain: kontrak interface ---
interface Penyimpanan {
fun simpan(kunci: String, nilai: String): Boolean
fun baca(kunci: String): String?
}
interface PemutusLog {
fun info(pesan: String)
fun error(pesan: String)
}
interface PabrikUuid {
fun baru(): String
}
// --- commonMain: logika bisnis yang memakai kontrak ---
class CatatanPengguna(
private val penyimpanan: Penyimpanan,
private val log: PemutusLog,
private val uuid: PabrikUuid
) {
fun buatCatatan(pengguna: String, isi: String): String {
require(pengguna.isNotBlank()) { "pengguna wajib" }
require(isi.length <= 280) { "isi maksimal 280 karakter" }
val id = uuid.baru()
val ok = penyimpanan.simpan("catatan:$id", "$pengguna|$isi")
if (ok) {
log.info("Catatan $id dibuat oleh $pengguna")
} else {
log.error("Gagal menyimpan catatan $id untuk $pengguna")
}
return id
}
fun bacaCatatan(id: String): String? {
val raw = penyimpanan.baca("catatan:$id")
if (raw == null) {
log.error("Catatan $id tidak ditemukan")
return null
}
val (pengguna, isi) = raw.split("|", limit = 2)
log.info("Catatan $id dibaca oleh $pengguna")
return isi
}
}
// --- Platform: JVM (memakai java.io, java.util.logging) ---
class PenyimpananJvm(private val dir: String) : Penyimpanan {
override fun simpan(kunci: String, nilai: String): Boolean = try {
java.io.File(dir, "$kunci.txt").writeText(nilai)
true
} catch (e: Exception) {
false
}
override fun baca(kunci: String): String? = try {
val f = java.io.File(dir, "$kunci.txt")
if (f.exists()) f.readText() else null
} catch (e: Exception) {
null
}
}
class LogJvm : PemutusLog {
override fun info(pesan: String) = println("[JVM/INFO] $pesan")
override fun error(pesan: String) = println("[JVM/ERROR] $pesan")
}
class UuidJvm : PabrikUuid {
private var counter = 0
override fun baru(): String = "jvm-000${counter++}"
}
// --- Platform: Android (simulasi) ---
class PenyimpananAndroid(private val dir: String) : Penyimpanan {
// Di Android nyata: Context.openFileOutput, SharedPreferences
private val map = mutableMapOf<String, String>()
override fun simpan(kunci: String, nilai: String): Boolean {
map[kunci] = nilai
return true
}
override fun baca(kunci: String): String? = map[kunci]
}
class LogAndroid : PemutusLog {
override fun info(pesan: String) = println("[Android/INFO] $pesan")
override fun error(pesan: String) = println("[Android/ERROR] $pesan")
}
class UuidAndroid : PabrikUuid {
private var counter = 0
override fun baru(): String = "android-${counter++}"
}
// --- Platform: iOS (simulasi) ---
class PenyimpananIos(private val dir: String) : Penyimpanan {
// Di iOS nyata: NSFileManager + NSUserDefaults
private val map = mutableMapOf<String, String>()
override fun simpan(kunci: String, nilai: String): Boolean {
map[kunci] = nilai
return true
}
override fun baca(kunci: String): String? = map[kunci]
}
class LogIos : PemutusLog {
override fun info(pesan: String) = println("[iOS/INFO] $pesan")
override fun error(pesan: String) = println("[iOS/ERROR] $pesan")
}
class UuidIos : PabrikUuid {
private var counter = 0
override fun baru(): String = "ios-${counter++}"
}
// --- Provider per platform ---
data class PlatformBundle(
val nama: String,
val penyimpanan: Penyimpanan,
val log: PemutusLog,
val uuid: PabrikUuid
)
fun main() {
val tmpDir = "/tmp/kmp-demo-platform"
java.io.File(tmpDir).mkdirs()
val bundles = listOf(
PlatformBundle("JVM",
PenyimpananJvm(tmpDir),
LogJvm(),
UuidJvm()),
PlatformBundle("Android",
PenyimpananAndroid(tmpDir),
LogAndroid(),
UuidAndroid()),
PlatformBundle("iOS",
PenyimpananIos(tmpDir),
LogIos(),
UuidIos())
)
for (bundle in bundles) {
println("=== Platform: ${bundle.nama} ===")
val catatan = CatatanPengguna(bundle.penyimpanan, bundle.log, bundle.uuid)
val id1 = catatan.buatCatatan("budi", "Belajar KMP hari ini")
val id2 = catatan.buatCatatan("siti", "Rapat jam 10 pagi")
println("Baca $id1: ${catatan.bacaCatatan(id1)}")
println("Baca catatan-tidak-ada: ${catatan.bacaCatatan("catatan:tidak-ada")}")
println()
}
java.io.File(tmpDir).deleteRecursively()
}Output yang Dihasilkan:
=== Platform: JVM ===
[JVM/INFO] Catatan jvm-0000 dibuat oleh budi
[JVM/INFO] Catatan jvm-0001 dibuat oleh siti
[JVM/INFO] Catatan jvm-0000 dibaca oleh budi
Baca jvm-0000: Belajar KMP hari ini
[JVM/ERROR] Catatan catatan:tidak-ada tidak ditemukan
Baca catatan-tidak-ada: null
=== Platform: Android ===
[Android/INFO] Catatan android-0 dibuat oleh budi
[Android/INFO] Catatan android-1 dibuat oleh siti
[Android/INFO] Catatan android-0 dibaca oleh budi
Baca android-0: Belajar KMP hari ini
[Android/ERROR] Catatan catatan:tidak-ada tidak ditemukan
Baca catatan-tidak-ada: null
=== Platform: iOS ===
[iOS/INFO] Catatan ios-0 dibuat oleh budi
[iOS/INFO] Catatan ios-1 dibuat oleh siti
[iOS/INFO] Catatan ios-0 dibaca oleh budi
Baca ios-0: Belajar KMP hari ini
[iOS/ERROR] Catatan catatan:tidak-ada tidak ditemukan
Baca catatan-tidak-ada: nullMari kita bedah bagian kunci dari kode di atas:
interface Penyimpanan,PemutusLog,PabrikUuid: tiga kontrak dicommonMain. Logika bisnisCatatanPenggunahanya bergantung pada ketiga interface ini, bukan pada API platform mana pun. Inilah inti pemisahan kode bisnis dari kode platform.class CatatanPengguna: kode bisnis bersama—validasi viarequire, generate UUID, simpan viaPenyimpanan.simpan(), log viaPemutusLog.info(). Seluruh kode ini berada dicommonMaindan tidak peduli platform.PenyimpananJvm: implementasi JVM yang memakaijava.io.Fileuntuk menulis dan membaca file. Pada proyek KMP nyata, class ini berada dijvmMain. Untuk Android dan iOS, kita simulasikan denganMutableMapkarena JVM tidak memilikiContextatauNSFileManager.LogJvm,LogAndroid,LogIos: tiga implementasi logging dengan prefix berbeda—[JVM/INFO],[Android/INFO],[iOS/INFO]. Di proyek nyata,LogAndroidmemakaiandroid.util.Log.i()danLogIosmemakaios_log.UuidJvm,UuidAndroid,UuidIos: tiga implementasi generator UUID. Di proyek nyata,UuidJvmmemakaijava.util.UUID.randomUUID(),UuidIosmemakaiFoundation.NSUUID(). Di sini kita pakai counter sederhana agar output deterministik.PlatformBundle: data class yang menggabungkan tiga dependency. Pola ini menyederhanakan konstruksi—kodemain()cukup memilih bundle, bukan menyuntikkan tiga dependency terpisah.- Output tiga platform: kode bisnis
CatatanPenggunayang sama menghasilkan struktur output identik—buatCatatanmengembalikan ID,bacaCatatanmengembalikan isi—dengan prefix log yang berbeda per platform.
Memilih expect/actual vs Interface #
Kapan memilih expect/actual dan kapan memilih interface? Aturan praktis: bila fungsi platform-specific berdiri sendiri dan tidak membutuhkan state, pakai expect fun. Bila komponen membutuhkan state, dipakai banyak class, atau ingin dapat diuji dengan mock, pakai interface. Kombinasi keduanya juga lazim—expect untuk fungsi kecil (mis. namaPlatform()), interface untuk komponen stateful (mis. Penyimpanan).
expect fun namaPlatform(): String // fungsi kecil, pakai expect
interface Penyimpanan { // komponen stateful, pakai interface
fun simpan(kunci: String, nilai: String): Boolean
fun baca(kunci: String): String?
}Referensi Lanjutan #
- Expect and actual declarations — dokumentasi resmi expect/actual untuk menulis implementasi spesifik platform
- Share code on platforms — panduan berbagi kode dengan jalur spesifik per platform
Penutup #
Kode platform-specific tidak bisa dihindari—file I/O, network, threading, dan logging berbeda di setiap platform. Yang dapat kita kendalikan adalah bagaimana kode bisnis berinteraksi dengan kode platform. Pola expect/actual untuk fungsi kecil dan injeksi interface untuk komponen stateful adalah dua teknik yang saling melengkapi. Chapter berikutnya akan masuk ke dependency management: bagaimana library KMP seperti kotlinx.coroutines dan ktor-client dideklarasikan di build.gradle.kts.