Naik versi Kotlin sering kali menimbulkan kekhawatiran: apakah kode lama tetap compile? Apakah library yang dipakai tetap kompatibel? Apakah hasil runtime berubah? Untungnya Kotlin punya kebijakan kompatibilitas yang jelas dan proses migrasi yang terstruktur. Pada chapter ini kita akan memahami tiga jenis kompatibilitas (biner, sumber, behavior), siklus deprecation yang bertahap, dan strategi migrasi yang aman untuk kode produksi.

Tiga Jenis Kompatibilitas #

Kotlin membedakan tiga jenis kompatibilitas yang harus kita pahami:

JenisPertanyaanGaransi Kotlin
BinaryApakah bytecode lama tetap berjalan?Ya — kotlinx-metadata menjamin
SourceApakah kode lama tetap compile?Ya — kecuali ada deprecation ERROR
BehaviorApakah hasil runtime berubah?Ya — kecuali bug fix yang didokumentasikan

Binary compatibility berarti kita bisa update compiler tanpa harus re-compile semua dependency — library yang dikompilasi dengan Kotlin 1.9 tetap bisa dipakai oleh kode Kotlin 2.0. Source compatibility berarti kode sumber yang lama tetap bisa dikompilasi dengan compiler baru, mungkin dengan deprecation warning. Behavior compatibility berarti hasil eksekusi tidak berubah — penting untuk reproducibility test.

Deprecation Cycle #

Kotlin menggunakan siklus deprecation bertahap untuk memberi waktu migrasi:

  1. WARNING (2 versi mayor sebelum removal) — kompilasi tetap berhasil, tetapi muncul warning.
  2. ERROR (1 versi mayor sebelum removal) — kompilasi gagal, kode harus diubah.
  3. Removal — API/fitur benar-benar dihapus.

Cuplikan berikut menunjukkan ketiga tingkat deprecation di kode:

kotlin
// compatibility_dan_migration.kt
// Mendemonstrasikan konsep kompatibilitas dan migrasi Kotlin:
// - binary compatibility (kotlin metadata)
// - language version dan api version
// - deprecation cycle (warning -> error -> removal)
// - migration tools (kotlin -Xfix)

// 1. Deprecation cycle — penggunaan @Deprecated
class PenyimpananLama {
    @Deprecated(
        message = "Gunakan ambilVersiBaru() — yang mengembalikan nullable",
        replaceWith = ReplaceWith("ambilVersiBaru()"),
        level = DeprecationLevel.WARNING
    )
    fun ambilVersiLama(): String = "data lama"

    fun ambilVersiBaru(): String? = null
}

// 2. Deprecation level ERROR — penggunaan langsung gagal compile
class ApiLama {
    @Deprecated(
        message = "API dihapus, migrasi ke ApiBaru",
        level = DeprecationLevel.ERROR
    )
    fun halu(): String = "halu"
}

class ApiBaru {
    fun halo(): String = "halo"
}

// 3. Binary compatibility — interface dan implementasi versi berbeda
interface Kontrak {
    val versi: Int
    fun proses(data: String): String
}

class ImplementasiLama : Kontrak {
    override val versi = 1
    override fun proses(data: String) = "v1:$data"
}

class ImplementasiBaru : Kontrak {
    override val versi = 2
    override fun proses(data: String) = "v2:$data (encoded)"
}

// 4. Language version dan API version
//    Di build.gradle.kts:
//      kotlin {
//        compilerOptions {
//          languageVersion.set(KotlinVersion.VERSION_2_0)
//          apiVersion.set(KotlinVersion.VERSION_2_0)
//        }
//      }
//    Untuk mengaktifkan fitur eksperimen:
//      freeCompilerArgs.add("-XXLanguage:+WhenGuards")

// 5. Migration helper — gunakan .replaceWith untuk auto-migrate
class Layanan {
    @Deprecated("Gunakan simpan(nama, data)", ReplaceWith("simpan(\"default\", data)"))
    fun simpan(data: String) = simpan("default", data)

    fun simpan(nama: String, data: String): String = "$nama=$data"
}

fun main() {
    println("=== Compatibility dan Migration ===")
    println()

    println("--- 1. Deprecation level WARNING ---")
    val lama = PenyimpananLama()
    println("Hasil: ${lama.ambilVersiBaru() ?: "(kosong)"}")
    println("Metode lama masih bisa dipanggil (dengan warning saat compile)")

    println()
    println("--- 2. Deprecation level ERROR ---")
    val baru = ApiBaru()
    println("ApiBaru.halo() = ${baru.halo()}")
    println("ApiLama.halu() gagal compile (DeprecationLevel.ERROR)")

    println()
    println("--- 3. Binary compatibility lewat interface ---")
    val daftar: List<Kontrak> = listOf(ImplementasiLama(), ImplementasiBaru())
    daftar.forEach {
        println("  versi ${it.versi}: ${it.proses("data-input")}")
    }

    println()
    println("--- 4. ReplaceWith untuk auto-migrate ---")
    val ls = Layanan()
    println("simpan(data=\"halo\")        = ${ls.simpan("halo")}")
    println("simpan(\"cache\", \"halo\")  = ${ls.simpan("cache", "halo")}")

    println()
    println("--- 5. Strategi migrasi ---")
    val strategi = listOf(
        "1. Update compiler ke versi target",
        "2. Set languageVersion dan apiVersion sesuai versi lama",
        "3. Jalankan build — perhatikan deprecation warning",
        "4. Migrasi bertahap menggunakan -Xfix=n",
        "5. Update languageVersion dan apiVersion ke versi baru",
        "6. Hapus kode kompatibilitas setelah semua dependensi dimigrasi"
    )
    strategi.forEach { println("  $it") }

    println()
    println("--- 6. Aturan kompatibilitas ---")
    val aturan = listOf(
        "Binary: bytecode tetap kompatibel (kotlin metadata menjamin)",
        "Source: kode lama harus tetap compile di compiler baru",
        "Behavior: hasil runtime tidak berubah (kecuali bug fix)",
        "Deprecation: 2 versi mayor sebelum removal (warning -> error -> hapus)"
    )
    aturan.forEach { println("  - $it") }
}

Output yang Dihasilkan:

text
=== Compatibility dan Migration ===

--- 1. Deprecation level WARNING ---
Hasil: (kosong)
Metode lama masih bisa dipanggil (dengan warning saat compile)

--- 2. Deprecation level ERROR ---
ApiBaru.halo() = halo
ApiLama.halu() gagal compile (DeprecationLevel.ERROR)

--- 3. Binary compatibility lewat interface ---
  versi 1: v1:data-input
  versi 2: v2:data-input (encoded)

--- 4. ReplaceWith untuk auto-migrate ---
simpan(data="halo")        = default=halo
simpan("cache", "halo")  = cache=halo

--- 5. Strategi migrasi ---
  1. Update compiler ke versi target
  2. Set languageVersion dan apiVersion sesuai versi lama
  3. Jalankan build — perhatikan deprecation warning
  4. Migrasi bertahap menggunakan -Xfix=n
  5. Update languageVersion dan apiVersion ke versi baru
  6. Hapus kode kompatibilitas setelah semua dependensi dimigrasi

--- 6. Aturan kompatibilitas ---
  - Binary: bytecode tetap kompatibel (kotlin metadata menjamin)
  - Source: kode lama harus tetap compile di compiler baru
  - Behavior: hasil runtime tidak berubah (kecuali bug fix)
  - Deprecation: 2 versi mayor sebelum removal (warning -> error -> hapus)

Pada cuplikan di atas, ambilVersiLama() menghasilkan warning saat dipanggil (level WARNING). ApiLama.halu() tidak bisa dipanggil sama sekali — kompilasi gagal (level ERROR). Anotasi ReplaceWith memberi tahu IntelliJ IDEA bahwa pemanggilan lama bisa otomatis diganti dengan sintaks baru — fitur quick-fix yang sangat membantu migrasi massal.

Language Version dan API Version #

Untuk memberi kontrol lebih granular, Kotlin menyediakan dua flag compiler:

  • languageVersion — mengontrol fitur bahasa (sintaks) yang tersedia.
  • apiVersion — mengontrol standard library yang tersedia.

Konfigurasi umum di Gradle:

kotlin build.gradle.kts — pinned language version
kotlin {
    compilerOptions {
        languageVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.VERSION_2_0)
        apiVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.VERSION_2_0)
    }
}

Bila kita memakai Kotlin 2.1 compiler tetapi ingin fitur tetap terbatas pada 2.0 (mis. untuk library yang dikonsumsi pengguna 2.0), set languageVersion = 2.0. Kompilator akan menolak fitur 2.1 seperti when guard.

Strategi Migrasi #

Berikut strategi migrasi yang aman untuk kode produksi:

  1. Update compiler ke versi target tanpa mengubah languageVersion / apiVersion dulu. Build seharusnya tetap berhasil dengan deprecation warning.
  2. Perbaiki deprecation warning satu per satu. Gunakan quick-fix IntelliJ IDEA (ReplaceWith) untuk migrasi otomatis.
  3. Naikkan apiVersion lebih dulu (standard library jarang breaking). Build lagi.
  4. Naikkan languageVersion paling akhir. Bila ada fitur baru yang mengubah perilaku, perbaiki kode yang terdampak.
  5. Hapus kode kompatibilitas setelah semua dependensi pengguna sudah dimigrasi.

Binary Compatibility di Library #

Pengelola library harus berhati-hati menjaga binary compatibility. Beberapa aturan praktis:

  • Jangan hapus method publik — gunakan deprecation WARNING dulu, tunggu 2 versi mayor, baru hapus.
  • Jangan ubah signature method publik — tambah overload baru, jangan ubah yang lama.
  • Tulis @JvmStatic, @JvmField, @JvmName bila ingin interoperabilitas dengan Java.
  • Gunakan kotlinx-binary-compatibility-validator — plugin Gradle yang memeriksa binary compatibility saat build.
kotlin Plugin binary-compatibility-validator
plugins {
    id("org.jetbrains.kotlinx.binary-compatibility-validator") version "0.16.3"
}

apiValidation {
    ignoredPackages.add("kotlinx.coroutines.internal")
    nonPublicMarkers.add("kotlin.PublishedApi")
}

Plugin ini menghasilkan file api/<module>.api yang dideteksi perubahan. Bila ada perubahan API yang tidak diinginkan, build gagal — mencegah kebocoran breaking change.

Deprecation Cycle di Standard Library #

Standard library Kotlin sendiri memakai siklus deprecation yang sama. Contoh:

  • kotlin.coroutines.experimental (Kotlin 1.1) → deprecated di 1.3 (WARNING) → dihapus di 1.4.
  • kotlin.test lama → migrasi ke kotlin.test baru dengan annotation berbeda.
  • Banyak extension function di kotlin-stdlib-jdk7 / jdk8 → sebagian dipindah ke kotlin-stdlib inti.

Referensi Lanjutan #

Selanjutnya #

Bagian modern Kotlin selesai. Section berikutnya (Sumber Belajar) akan mengarahkan kita ke dokumentasi resmi, API reference, dan panduan spesifik per domain — agar kita tahu ke mana harus mencari jawaban saat menemui masalah nyata.