Beberapa property perlu memicu tindakan otomatis setiap kali nilainya berubah: mencatat ke audit log, memperbarui UI, atau memicu perhitungan ulang. Tanpa delegation, kita harus menulis setter manual pada setiap property — pekerjaan yang membosankan dan rentan terlupakan. Kotlin menyediakan Delegates.observable yang memanggil callback secara otomatis setelah assignment. Pada chapter ini kita akan membahas kontrak callback observable, perbedaannya dengan vetoable, dan penerapannya pada tiga kasus: penghitung, audit nama, dan sensor suhu. Materi ini menjadi dasar sebelum kita membahas vetoable yang dapat menolak perubahan pada chapter berikutnya.
Kontrak observable #
Function Delegates.observable menerima dua argumen: nilai awal dan lambda (prop, old, new) -> Unit. Lambda dipanggil setelah nilai baru ditulis ke field. Sintaksisnya ringkas:
var x: Int by Delegates.observable(0) { prop, old, new ->
println("${prop.name}: $old -> $new")
}| Komponen | Tipe | Keterangan |
|---|---|---|
prop | KProperty<*> | Reference ke property yang berubah; prop.name mengembalikan nama property. |
old | T | Nilai sebelum perubahan. |
new | T | Nilai baru yang sudah ditulis. |
Karena lambda dipanggil setelah assignment, kita tidak dapat menolak perubahan dari dalam observable. Untuk fungsi reject, gunakan Delegates.vetoable yang akan dibahas pada chapter berikutnya.
Tiga Pola Penggunaan #
File observable_properties.kt menunjukkan tiga skenario: penghitung sederhana, audit nama pengguna, dan sensor suhu yang memperingatkan perubahan drastis.
// File: observable_properties.kt
// Mendemonstrasikan observable: callback dipanggil setelah nilai berubah.
package com.example.observ
import kotlin.properties.Delegates
class Penghitung {
// observable memanggil lambda SETELAH nilai di-assign.
var hitung: Int by Delegates.observable(0) { prop, old, new ->
println("[observable] ${prop.name}: $old -> $new")
}
// Bisa dipakai untuk audit log tanpa boilerplate setter manual.
var nama: String by Delegates.observable("anonim") { prop, old, new ->
println("[audit] ${prop.name} berubah dari \"$old\" menjadi \"$new\"")
}
}
class SuhuBerjalan {
// Menyimpan riwayat semua nilai yang pernah diset.
private val _riwayat = mutableListOf<Int>()
val riwayat: List<Int> get() = _riwayat.toList()
var suhu: Int by Delegates.observable(25) { _, old, new ->
_riwayat.add(new)
if (kotlin.math.abs(new - old) > 5) {
println("[peringatan] Suhu berubah drastis: $old -> $new")
}
}
}
fun main() {
val hitung = Penghitung()
hitung.hitung = 1
hitung.hitung = 2
hitung.hitung = 3
println("Akhir hitung: ${hitung.hitung}")
println()
val app = Penghitung()
app.nama = "Budi"
app.nama = "Siti"
println("Nama akhir: ${app.nama}")
println()
val sensor = SuhuBerjalan()
sensor.suhu = 26
sensor.suhu = 28
sensor.suhu = 35 // perubahan besar — memicu peringatan
println("Riwayat suhu: ${sensor.riwayat}")
}Output yang Dihasilkan:
[observable] hitung: 0 -> 1
[observable] hitung: 1 -> 2
[observable] hitung: 2 -> 3
Akhir hitung: 3
[audit] nama berubah dari "anonim" menjadi "Budi"
[audit] nama berubah dari "Budi" menjadi "Siti"
Nama akhir: Siti
[peringatan] Suhu berubah drastis: 28 -> 35
Riwayat suhu: [26, 28, 35]Mari kita bedah baris penting pada kode di atas:
- Baris 7-16 (
class Penghitung): dua propertyobservable. Lambda pertama mencetak nama property bersama nilai lama dan baru; lambda kedua memakai format audit yang lebih deskriptif. Perhatikan bahwa callback dipanggil setiap kali assignment terjadi, termasuk ketika nilai baru sama dengan nilai lama. - Baris 19-30 (
class SuhuBerjalan): pola side effect yang lebih kompleks. Setiap perubahansuhumenambahkannewke_riwayat(list internal). Bila selisihnew - oldmelebihi 5, pesan peringatan dicetak. Propertyriwayatbersifat publik namun read-only — getter-nya mengembalikan salinan list agar mutasi dari luar tidak mengganggu state internal. - Baris 24 (
var suhu: Int by Delegates.observable(25) { _, old, new -> ... }): parameter pertama lambda (prop) tidak terpakai sehingga diganti dengan_. Ini adalah idiom Kotlin untuk parameter yang diabaikan. - Baris 33-37 (
hitung.hitung = 1; 2; 3): setiap assignment memicu callback sehingga tiga baris[observable] hitung: ...muncul sebelum barisAkhir hitung. - Baris 46-50 (
sensor.suhu = 26; 28; 35): dua perubahan pertama (25→26 dan 26→28) tidak memicu peringatan karena selisihnya ≤ 5. Perubahan terakhir (28→35) memicu peringatan karena selisihnya 7.riwayatakhir berisi[26, 28, 35]— semua nilai yang pernah diset, tidak termasuk nilai awal25karena callback hanya dipanggil saat assignment, bukan saat inisialisasi.
observable vs Getter Manual #
Pola yang sama dapat ditulis dengan backing field plus custom setter, namun observable merangkum boilerplate tersebut. Bandingkan dua cuplikan berikut:
// 1) Backing field manual
private var _hitung = 0
var hitung: Int
get() = _hitung
set(value) {
val old = _hitung
_hitung = value
println("[observable] hitung: $old -> $value")
}
// 2) observable — logika sama, lebih ringkas
var hitung: Int by Delegates.observable(0) { _, old, new ->
println("[observable] hitung: $old -> $new")
}Kedua pendekatan menghasilkan perilaku identik, tetapi observable memindahkan tanggung jawab ke delegate dan menyembunyikan backing field. Hal ini sangat berguna bila pola reaksi terhadap perubahan dipakai pada banyak property dalam satu class.
Referensi Lanjutan #
- Delegated properties
— bagian
Delegates.observablepada panduan resmi - Delegates object
— API reference object
Delegatesyang menyediakanobservable
Selanjutnya #
Chapter vetoable Properties akan memperkenalkan delegate yang dapat menolak perubahan nilai berdasarkan kondisi validasi — pendekatan yang lebih cocok untuk constraint domain seperti nilai tidak boleh negatif atau email harus mengandung @.