kotlinadvanced

Custom Property Delegates

Create reusable property delegates: validation, logging, caching, and thread-safe lazy initialization.

kotlin
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty

// Validated delegate
class Validated<T>(
    private var value: T,
    private val validator: (T) -> Boolean,
    private val errorMessage: (T) -> String = { "Invalid value: $it" }
) : ReadWriteProperty<Any?, T> {
    override fun getValue(thisRef: Any?, property: KProperty<*>): T = value
    override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
        require(validator(value)) { errorMessage(value) }
        this.value = value
    }
}

fun <T> validated(initial: T, message: String = "", validator: (T) -> Boolean) =
    Validated(initial, validator) { if (message.isNotEmpty()) message else "Invalid: $it" }

// Logging delegate
class LoggingDelegate<T>(private var value: T) : ReadWriteProperty<Any?, T> {
    override fun getValue(thisRef: Any?, property: KProperty<*>): T {
        println("[GET] ${property.name} = $value")
        return value
    }
    override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
        println("[SET] ${property.name}: ${this.value} → $value")
        this.value = value
    }
}

fun <T> logged(initial: T) = LoggingDelegate(initial)

// Expiring cache delegate
class ExpiringCache<T>(
    private val ttlMs: Long,
    private val loader: () -> T
) : ReadWriteProperty<Any?, T> {
    private var cachedValue: T? = null
    private var lastLoad: Long = 0

    override fun getValue(thisRef: Any?, property: KProperty<*>): T {
        val now = System.currentTimeMillis()
        if (cachedValue == null || now - lastLoad > ttlMs) {
            cachedValue = loader()
            lastLoad = now
        }
        return cachedValue!!
    }

    override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
        cachedValue = value
        lastLoad = System.currentTimeMillis()
    }
}

fun <T> cached(ttlMs: Long, loader: () -> T) = ExpiringCache(ttlMs, loader)

// Observable with veto power
class VetoableObservable<T>(
    private var value: T,
    private val onChange: (old: T, new: T) -> Boolean
) : ReadWriteProperty<Any?, T> {
    override fun getValue(thisRef: Any?, property: KProperty<*>) = value
    override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
        if (onChange(this.value, value)) {
            this.value = value
        }
    }
}

// Trimmed string delegate
object TrimmedString {
    operator fun provideDelegate(
        thisRef: Any?,
        property: KProperty<*>
    ): ReadWriteProperty<Any?, String> {
        return object : ReadWriteProperty<Any?, String> {
            private var value = ""
            override fun getValue(thisRef: Any?, property: KProperty<*>) = value
            override fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
                this.value = value.trim()
            }
        }
    }
}

// Usage
class UserProfile {
    var name: String by logged("")
    var age: Int by validated(0, "Age must be 0-150") { it in 0..150 }
    var email: String by validated("") { it.contains("@") || it.isEmpty() }
    var bio: String by TrimmedString

    // Built-in delegates
    val greeting: String by lazy { "Hello, $name!" }
    val properties: MutableMap<String, Any?> = mutableMapOf()
    var nickname: String by properties.withDefault { "" }
}

fun main() {
    val profile = UserProfile()

    // Logging delegate
    profile.name = "Alice"
    println("Name is: ${profile.name}")

    // Validated
    profile.age = 30
    println("Age: ${profile.age}")
    try { profile.age = 200 } catch (e: Exception) { println("Error: ${e.message}") }

    // Trimmed
    profile.bio = "  Hello World!  "
    println("Bio: '${profile.bio}'")

    // Lazy
    println(profile.greeting)

    // Map-backed
    profile.nickname = "Ali"
    println("Nickname: ${profile.nickname}")
    println("Properties map: ${profile.properties}")

    // Caching delegate
    var config: Map<String, String> by cached(5000) {
        println("Loading config...")
        mapOf("key" to "value-${System.currentTimeMillis()}")
    }
    println(config)
    println(config) // cached, no reload
}

Use Cases

  • Input validation on property assignment
  • Debug logging for state changes
  • Cached values with TTL expiration

Tags

Related Snippets

Similar patterns you can reuse in the same workflow.