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:

PendekatanUntuk valUntuk varCocok bila
class : ReadOnlyProperty<R,T>YaTidakDelegate read-only yang ingin diperlakukan sebagai tipe eksplisit.
class : ReadWriteProperty<R,T>YaYaDelegate read-write yang ingin diperlakukan sebagai tipe eksplisit.
Member operator fun getValue / setValueYaYaDelegate 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:

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

text
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-label

Mari kita bedah bagian-bagian kunci pada kode di atas:

  1. Baris 11-21 (class PropertiLingkungan : ReadOnlyProperty<Any?, String>): delegate val. Konstruktor menerima default dan key. getValue membaca dari simulasiEnv[key] (simulasi System.getenv) dan mengembalikan default bila key tidak ada. Tiga property pada class Aplikasi memakai delegate ini dengan parameter berbeda — namun tetap memegang logika lookup yang sama.
  2. Baris 24-32 (class PropertiTerkonversi<T> : ReadWriteProperty<Any?, T>): delegate var generik. setValue tidak menyimpan value apa adanya; nilai terlebih dahulu dilewatkan ke lambda konversi. Dengan demikian, app.tag = "servis" menyimpan "SERVIS" (uppercase), dan app.jumlah = 150 menyimpan 100 (coerced ke rentang 0..100). Hal ini mendemonstrasikan bahwa delegate dapat melakukan transformasi sederhana yang sebelumnya sering ditulis manual pada setter.
  3. Baris 35-39 (class Pemformat dengan operator fun getValue): pendekatan ketiga — tidak mengimplementasikan interface, hanya menyediakan method operator fun getValue. Kompilator tetap menerima ini sebagai delegate val. Pola ini berguna bila kita ingin delegate ringkas yang tidak terikat kontrak interface standard library.
  4. Baris 47-55 (deklarasi property pada Aplikasi): lima property memakai ketiga delegate. nama, port, tidakAda memakai PropertiLingkungan; tag dan jumlah memakai PropertiTerkonversi; label memakai Pemformat. Perhatikan bahwa tipe label tidak ditulis secara eksplisit — kompilator menyimpulkan String dari getValue yang mengembalikan String.
  5. Baris 65-70 (app.jumlah = 150 lalu app.jumlah = -5): dua assignment yang sama-sama menjalankan lambda coerceIn(0, 100). Assignment pertama menghasilkan 100 (150 di-clip ke batas atas), assignment kedua menghasilkan 0 (-5 di-clip ke batas bawah). Ini memperlihatkan bahwa nilai yang dikembalikan getValue selalu 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 #

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.