Pada delegation biasa, objek delegate dibuat lebih dulu (misalnya EnvProvider()), lalu kompilator mengikat property kepadanya lewat getValue/setValue. Alur ini tidak memberi delegate kesempatan untuk memeriksa metadata property seperti nama, tipe, atau annotation saat delegate dibuat — metadata hanya tersedia saat getValue/setValue dipanggil di akses runtime. Untuk kebutuhan seperti membaca annotation @DariEnv("DATABASE_URL") agar key environment dapat ditentukan sekali saat inisialisasi, Kotlin menyediakan operator provideDelegate. Pada chapter ini kita akan membahas kontraknya, kapan dipanggil, dan penerapannya pada delegate berbasis annotation.

Kontrak provideDelegate #

Operator provideDelegate adalah extension function dengan signature berikut:

kotlin Signature umum provideDelegate
operator fun D.provideDelegate(
    thisRef: R,
    property: KProperty<*>
): D

Kompilator memanggil provideDelegate sekali saat deklarasi property diproses — yaitu saat instance class pemilik dibuat. Nilai yang dikembalikan oleh provideDelegate menjadi delegate sebenarnya yang dipakai untuk getValue/setValue selanjutnya. Bila provideDelegate tidak didefinisikan, kompilator memakai objek delegate apa adanya.

TahapKapan dipanggilAkses metadata
Pembuatan delegateSaat instance class dibuatprovideDelegate(thisRef, property)
Pembacaan propertySetiap akses getgetValue(thisRef, property)
Penulisan propertySetiap akses setsetValue(thisRef, property, value)

Karena provideDelegate menerima property: KProperty<*>, delegate dapat memeriksa property.name, property.returnType, dan property.annotations sebelum memutuskan delegate mana yang akan dipakai.

Kasus Penggunaan: Environment Berbasis Annotation #

File provide_delegate.kt menunjukkan pola umum: sebuah annotation @DariEnv("KEY") dipakai untuk menandai property yang harus diisi dari environment variable. Tanpa provideDelegate, delegate harus membaca annotation pada setiap getValue — boros. Dengan provideDelegate, delegate akhir (EnvDelegate) sudah mengetahui key yang benar sejak awal.

kotlin
// File: provide_delegate.kt
// Mendemonstrasikan operator provideDelegate: inspeksi metadata property
// sebelum delegate sebenarnya dibuat.
package com.example.provide

import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty

// Annotation untuk menandai property yang wajib diisi dari environment.
@Target(AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.RUNTIME)
annotation class DariEnv(val key: String)

// Delegate yang membaca dari map "environment".
class EnvDelegate(private val key: String, private val default: String) : ReadOnlyProperty<Any?, String> {
    override fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return simulasiEnv[key] ?: default
    }
}

// Provider: dipanggil sekali saat deklarasi property diproses.
// Di sini kita membaca annotation @DariEnv untuk menentukan key.
class EnvProvider : ReadOnlyProperty<Any?, String> {
    override fun getValue(thisRef: Any?, property: KProperty<*>): String {
        // Fallback bila tidak ada @DariEnv.
        return simulasiEnv[property.name] ?: property.name
    }
}

// Operator provideDelegate: inspeksi property metadata sebelum membentuk delegate.
operator fun ReadOnlyProperty<Any?, String>.provideDelegate(
    thisRef: Any?,
    property: KProperty<*>
): ReadOnlyProperty<Any?, String> {
    val ann = property.annotations.filterIsInstance<DariEnv>().firstOrNull()
    return if (ann != null) {
        println("[provideDelegate] property=${property.name} membaca env key=${ann.key}")
        EnvDelegate(ann.key, default = "(tidak ada env)")
    } else {
        println("[provideDelegate] property=${property.name} tanpa @DariEnv — pakai default")
        this
    }
}

private val simulasiEnv = mapOf(
    "DATABASE_URL" to "jdbc:postgresql://localhost/app",
    "APP_PORT" to "8080",
    "LOG_LEVEL" to "INFO"
)

class Konfigurasi {
    @DariEnv("DATABASE_URL")
    val database: String by EnvProvider()

    @DariEnv("APP_PORT")
    val port: String by EnvProvider()

    // Tanpa @DariEnv — fallback memakai nama property sebagai key.
    val LOG_LEVEL: String by EnvProvider()

    val tidakDitandai: String by EnvProvider()
}

fun main() {
    val cfg = Konfigurasi()
    println()
    println("database: ${cfg.database}")
    println("port: ${cfg.port}")
    println("LOG_LEVEL: ${cfg.LOG_LEVEL}")
    println("tidakDitandai: ${cfg.tidakDitandai}")
}

Output yang Dihasilkan:

text
[provideDelegate] property=database membaca env key=DATABASE_URL
[provideDelegate] property=port membaca env key=APP_PORT
[provideDelegate] property=LOG_LEVEL tanpa @DariEnv — pakai default
[provideDelegate] property=tidakDitandai tanpa @DariEnv — pakai default

database: jdbc:postgresql://localhost/app
port: 8080
LOG_LEVEL: INFO
tidakDitandai: tidakDitandai

Mari kita bedah elemen kunci pada kode di atas:

  1. Baris 12-13 (annotation class DariEnv(val key: String)): annotation custom dengan target PROPERTY dan retention RUNTIME agar dapat dibaca via reflection. Parameter key menentukan nama environment variable yang akan dipakai.
  2. Baris 16-20 (class EnvDelegate): delegate sebenarnya yang sudah diberi tahu key dan default. getValue hanya membaca map; tidak perlu memeriksa annotation lagi pada setiap akses. Inilah efisiensi yang diperoleh dari provideDelegate.
  3. Baris 23-28 (class EnvProvider): delegate default yang dipakai bila annotation @DariEnv tidak ada. getValue memakai property.name sebagai key — artinya property LOG_LEVEL akan otomatis membaca key LOG_LEVEL dari environment.
  4. Baris 31-43 (extension provideDelegate pada ReadOnlyProperty<Any?, String>): ini adalah inti pola. Operator didefinisikan sebagai extension function pada tipe delegate apa pun yang mengimplementasikan ReadOnlyProperty<Any?, String>. Saat deklarasi val database: String by EnvProvider() diproses, kompilator memanggil extension ini dengan property = KProperty untuk database. Lambda membaca annotation @DariEnv dari property.annotations; bila ada, delegate diganti dengan EnvDelegate yang mengetahui key spesifik. Bila tidak ada, delegate asli (this) dipakai apa adanya.
  5. Baris 51-61 (class Konfigurasi): empat property memakai EnvProvider() — namun hanya tiga yang diberi annotation @DariEnv. Property LOG_LEVEL tidak diberi annotation tetapi nama property-nya persis sama dengan key environment, sehingga delegate default tetap berhasil membaca nilainya. Property tidakDitandai tidak memiliki annotation dan namanya tidak cocok dengan key manapun di map, sehingga getValue mengembalikan property.name itu sendiri yaitu "tidakDitandai".
  6. Baris 64-70 (main): sebuah baris kosong dicetak setelah instance Konfigurasi dibuat. Hal ini terjadi karena provideDelegate sudah mencetak pesan saat konstruksi, sebelum println() pertama pada main dijalankan. Urutan output membuktikan bahwa provideDelegate dipanggil pada saat instance class dibuat, bukan saat property pertama kali diakses.

Kapan Memakai provideDelegate #

provideDelegate paling berguna ketika delegate akhir bergantung pada metadata property yang hanya tersedia via reflection — annotation, nama property, atau tipe property. Tanpa provideDelegate, inspeksi metadata harus dilakukan pada setiap getValue/setValue yang berarti beban refleksi berulang setiap akses. Dengan provideDelegate, inspeksi cukup dilakukan sekali saat instance class dibuat.

Referensi Lanjutan #

  • Delegated properties — bagian provideDelegate pada panduan delegated properties
  • Reflection — panduan resmi reflection yang dipakai provideDelegate via KProperty

Selanjutnya #

Setelah memahami semua mekanisme delegation — lazy, observable, vetoable, map-backed, custom delegate, dan provideDelegate — chapter penutup akan merangkum praktik terbaik: kapan memakai masing-masing pola, jebakan yang harus dihindari, dan pedoman memilih delegate yang tepat untuk kebutuhan tertentu.