Delegate bawaan standard library (lazy, observable, vetoable, map-backed) sudah membantu banyak kasus. Namun bila kebutuhan kita lebih spesifik — misalnya membaca environment variable dengan default, mengubah nilai saat penugasan, atau mengikat property ke sumber data kustom — kita dapat menulis delegate sendiri. Pada chapter ini kita akan membahas tiga cara membuat custom delegate: mengimplementasikan ReadOnlyProperty, mengimplementasikan ReadWriteProperty, dan menyediakan operator fun getValue/setValue tanpa interface. Kode contoh akan memperlihatkan ketiganya dalam satu file yang sama.
Tiga Pendekatan #
Kompilator Kotlin tidak peduli apakah delegate mengimplementasikan interface tertentu; yang penting adalah method getValue dan (untuk var) setValue tersedia sebagai member function atau extension function dengan tanda operator. Tiga pendekatan umum dirangkum pada tabel berikut:
| Pendekatan | Untuk val | Untuk var | Cocok bila |
|---|---|---|---|
class : ReadOnlyProperty<R,T> | Ya | Tidak | Delegate read-only yang ingin diperlakukan sebagai tipe eksplisit. |
class : ReadWriteProperty<R,T> | Ya | Ya | Delegate read-write yang ingin diperlakukan sebagai tipe eksplisit. |
Member operator fun getValue / setValue | Ya | Ya | Delegate ringkas tanpa formalitas interface. |
ReadOnlyProperty dan ReadWriteProperty tersedia di package kotlin.properties. Kedua interface hanya berisi method operator yang sama sehingga bila sebuah class mengimplementasikan ReadWriteProperty, class tersebut otomatis memenuhi kontrak ReadOnlyProperty juga.
Contoh Kode #
File custom_property_delegates.kt memperagakan ketiga pendekatan dalam satu file:
// File: custom_property_delegates.kt
// Mendemonstrasikan cara membuat custom property delegate
// dengan ReadOnlyProperty / ReadWriteProperty dan operator getValue/setValue.
package com.example.custom
import kotlin.properties.ReadOnlyProperty
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
// 1. ReadOnlyProperty: untuk val dengan logika kustom.
class PropertiLingkungan(
private val default: String,
private val key: String
) : ReadOnlyProperty<Any?, String> {
override fun getValue(thisRef: Any?, property: KProperty<*>): String {
// Pada lingkungan nyata, baca System.getenv(key).
// Di sini kita simulasi: baca dari map sederhana.
val dariMap = simulasiEnv[key]
return dariMap ?: default
}
}
// 2. ReadWriteProperty: untuk var dengan logika kustom.
class PropertiTerkonversi<T>(
private var nilai: T,
private val konversi: (T) -> T
) : ReadWriteProperty<Any?, T> {
override fun getValue(thisRef: Any?, property: KProperty<*>): T = nilai
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
nilai = konversi(value)
}
}
// 3. Delegate via extension function getValue/setValue (tanpa implement interface).
class Pemformat {
// operator getValue on String? -> memberi nilai default bila null.
operator fun getValue(thisRef: Any?, property: KProperty<*>): String =
"formattted-${property.name}"
}
// Simulasi environment variables.
private val simulasiEnv = mapOf(
"APP_NAME" to "Aplikasi Demo",
"PORT" to "8080"
)
class Aplikasi {
val nama: String by PropertiLingkungan(default = "default-app", key = "APP_NAME")
val port: String by PropertiLingkungan(default = "8080", key = "PORT")
val tidakAda: String by PropertiLingkungan(default = "fallback", key = "TIDAK_ADA")
var tag: String by PropertiTerkonversi("awal") { it.uppercase() }
var jumlah: Int by PropertiTerkonversi(0) { it.coerceIn(0, 100) }
val label by Pemformat()
}
fun main() {
val app = Aplikasi()
println("nama: ${app.nama}")
println("port: ${app.port}")
println("tidakAda: ${app.tidakAda}")
println()
app.tag = "servis"
println("tag setelah uppercase: ${app.tag}")
app.jumlah = 150
println("jumlah setelah coerce(0,100): ${app.jumlah}")
app.jumlah = -5
println("jumlah setelah coerce(0,100) dari -5: ${app.jumlah}")
println()
println("label: ${app.label}")
}Output yang Dihasilkan:
nama: Aplikasi Demo
port: 8080
tidakAda: fallback
tag setelah uppercase: SERVIS
jumlah setelah coerce(0,100): 100
jumlah setelah coerce(0,100) dari -5: 0
label: formattted-labelMari kita bedah bagian-bagian kunci pada kode di atas:
- Baris 11-21 (
class PropertiLingkungan : ReadOnlyProperty<Any?, String>): delegateval. Konstruktor menerimadefaultdankey.getValuemembaca darisimulasiEnv[key](simulasiSystem.getenv) dan mengembalikandefaultbila key tidak ada. Tiga property pada classAplikasimemakai delegate ini dengan parameter berbeda — namun tetap memegang logika lookup yang sama. - Baris 24-32 (
class PropertiTerkonversi<T> : ReadWriteProperty<Any?, T>): delegatevargenerik.setValuetidak menyimpanvalueapa adanya; nilai terlebih dahulu dilewatkan ke lambdakonversi. Dengan demikian,app.tag = "servis"menyimpan"SERVIS"(uppercase), danapp.jumlah = 150menyimpan100(coerced ke rentang 0..100). Hal ini mendemonstrasikan bahwa delegate dapat melakukan transformasi sederhana yang sebelumnya sering ditulis manual pada setter. - Baris 35-39 (
class Pemformatdenganoperator fun getValue): pendekatan ketiga — tidak mengimplementasikan interface, hanya menyediakan methodoperator fun getValue. Kompilator tetap menerima ini sebagai delegateval. Pola ini berguna bila kita ingin delegate ringkas yang tidak terikat kontrak interface standard library. - Baris 47-55 (deklarasi property pada
Aplikasi): lima property memakai ketiga delegate.nama,port,tidakAdamemakaiPropertiLingkungan;tagdanjumlahmemakaiPropertiTerkonversi;labelmemakaiPemformat. Perhatikan bahwa tipelabeltidak ditulis secara eksplisit — kompilator menyimpulkanStringdarigetValueyang mengembalikanString. - Baris 65-70 (
app.jumlah = 150laluapp.jumlah = -5): dua assignment yang sama-sama menjalankan lambdacoerceIn(0, 100). Assignment pertama menghasilkan100(150 di-clip ke batas atas), assignment kedua menghasilkan0(-5 di-clip ke batas bawah). Ini memperlihatkan bahwa nilai yang dikembalikangetValueselalu merupakan hasil transformasi, bukan nilai asli yang ditugaskan.
Memilih Pendekatan #
Pilih ReadOnlyProperty atau ReadWriteProperty bila Anda ingin delegate menjadi tipe yang dapat dipakai lintas modul dan diperlakukan sebagai library class. Pendekatan ini juga memudahkan bila Anda ingin delegate memenuhi kontrak formal untuk tooling atau framework yang memeriksa is ReadOnlyProperty<*,*> pada saat reflection. Pilih pendekatan operator function murni bila delegate bersifat lokal dan ringkas; ini menghemat boilerplate deklarasi interface.
Referensi Lanjutan #
- Delegated properties — panduan resmi cara menulis custom property delegate
- ReadOnlyProperty
— API reference interface
ReadOnlyPropertyuntuk delegateval - ReadWriteProperty
— API reference interface
ReadWritePropertyuntuk delegatevar
Selanjutnya #
Kadang kita perlu memeriksa metadata property — misalnya nama atau annotation — sebelum delegate dibuat. Chapter provideDelegate akan membahas operator provideDelegate yang memberi kita kesempatan inspeksi metadata tersebut pada saat deklarasi diproses.