kotlinintermediate

Delegation — by, lazy, observable, and Custom

Use Kotlin delegation for reusable behavior: by keyword, lazy, observable, vetoable, and map-backed.

kotlin
import kotlin.properties.Delegates
import kotlin.reflect.KProperty

// Interface delegation (composition over inheritance)
interface Logger {
    fun log(message: String)
}

class ConsoleLogger : Logger {
    override fun log(message: String) = println("[LOG] $message")
}

class FileLogger : Logger {
    override fun log(message: String) = println("[FILE] $message")
}

// Delegate logging behavior
class UserService(logger: Logger) : Logger by logger {
    fun createUser(name: String) {
        log("Creating user: $name") // Delegated to logger
    }
}

// Property delegation
class AppConfig {
    // lazy — computed once on first access
    val databaseUrl: String by lazy {
        println("Computing database URL...")
        "jdbc:postgresql://localhost:5432/app"
    }

    // observable — callback on change
    var theme: String by Delegates.observable("light") { _, old, new ->
        println("Theme changed: $old → $new")
    }

    // vetoable — reject invalid values
    var fontSize: Int by Delegates.vetoable(14) { _, _, new ->
        new in 8..72 // only accept valid sizes
    }

    // notNull — must be set before first access
    var apiKey: String by Delegates.notNull()
}

// Custom delegate
class Trimmed {
    private var value: String = ""

    operator fun getValue(thisRef: Any?, property: KProperty<*>): String = value
    operator fun setValue(thisRef: Any?, property: KProperty<*>, newValue: String) {
        value = newValue.trim()
    }
}

// Cached delegate with expiration
class Cached<T>(private val ttlMs: Long, private val compute: () -> T) {
    private var cachedValue: T? = null
    private var lastComputed: Long = 0

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

// Map-backed properties
class MappedUser(map: Map<String, Any?>) {
    val name: String by map
    val age: Int by map
    val email: String by map
}

class Form {
    var username: String by Trimmed()
    var bio: String by Trimmed()
    val timestamp: Long by Cached(5000) { System.currentTimeMillis() }
}

fun main() {
    // Interface delegation
    val service = UserService(ConsoleLogger())
    service.createUser("Alice") // [LOG] Creating user: Alice

    // Lazy
    val config = AppConfig()
    println("Before access") // lazy not computed yet
    println(config.databaseUrl) // Now computed
    println(config.databaseUrl) // Cached

    // Observable
    config.theme = "dark" // Theme changed: light → dark

    // Vetoable
    config.fontSize = 20  // accepted
    config.fontSize = 100  // rejected (stays 20)
    println("Font size: ${config.fontSize}") // 20

    // Custom delegate
    val form = Form()
    form.username = "  alice  "
    form.bio = "  Hello world  "
    println("'${form.username}'") // 'alice'
    println("'${form.bio}'")      // 'Hello world'

    // Map-backed
    val user = MappedUser(mapOf("name" to "Bob", "age" to 25, "email" to "bob@test.com"))
    println("${user.name}, ${user.age}") // Bob, 25
}

Use Cases

  • Composing behavior without deep inheritance
  • Lazy initialization of expensive resources
  • Validation and formatting via property delegates

Tags

Related Snippets

Similar patterns you can reuse in the same workflow.