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:

kotlin Bentuk umum observable
var x: Int by Delegates.observable(0) { prop, old, new ->
    println("${prop.name}: $old -> $new")
}
KomponenTipeKeterangan
propKProperty<*>Reference ke property yang berubah; prop.name mengembalikan nama property.
oldTNilai sebelum perubahan.
newTNilai 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.

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

text
[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:

  1. Baris 7-16 (class Penghitung): dua property observable. 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.
  2. Baris 19-30 (class SuhuBerjalan): pola side effect yang lebih kompleks. Setiap perubahan suhu menambahkan new ke _riwayat (list internal). Bila selisih new - old melebihi 5, pesan peringatan dicetak. Property riwayat bersifat publik namun read-only — getter-nya mengembalikan salinan list agar mutasi dari luar tidak mengganggu state internal.
  3. 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.
  4. Baris 33-37 (hitung.hitung = 1; 2; 3): setiap assignment memicu callback sehingga tiga baris [observable] hitung: ... muncul sebelum baris Akhir hitung.
  5. 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. riwayat akhir berisi [26, 28, 35] — semua nilai yang pernah diset, tidak termasuk nilai awal 25 karena 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:

kotlin Backing field manual vs observable
// 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 #

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 @.