cyberangles guide

How to Use Kotlin’s Delegated Properties

Kotlin’s delegated properties are a powerful feature that enables you to **encapsulate and reuse property logic** by delegating the responsibility of getting and setting values to a separate class. Instead of writing repetitive getter/setter code for properties (e.g., logging, validation, lazy initialization), you can offload this logic to a delegate, promoting cleaner, more maintainable code. Whether you’re working with simple properties or complex state management, delegated properties help reduce boilerplate and enforce separation of concerns. In this guide, we’ll explore what delegated properties are, how they work, and how to leverage them effectively—from using Kotlin’s built-in delegates to creating your own custom implementations.

Table of Contents

  1. What Are Delegated Properties?
  2. The Delegation Pattern in Kotlin
  3. Standard Delegates Provided by Kotlin
  4. Creating Custom Delegates
  5. Advanced Use Cases
  6. Best Practices
  7. Conclusion
  8. References

What Are Delegated Properties?

In Kotlin, a delegated property is a property whose getter and setter logic is delegated to a separate object (the “delegate”). Instead of implementing the property’s access logic directly in the class, you outsource it to a delegate, which handles getting and setting values.

This pattern promotes:

  • Code reuse: Share property logic across multiple classes.
  • Cleaner code: Separate property logic from the class’s core responsibility.
  • Reduced boilerplate: Avoid repetitive getter/setter implementations.

The syntax for a delegated property is:

val/var <propertyName>: <Type> by <delegate>

Here, by specifies the delegate object. The delegate must implement specific methods to handle property access:

  • For read-only properties (val): A getValue method.
  • For mutable properties (var): Both getValue and setValue methods.

The Delegation Pattern in Kotlin

Delegated properties rely on the delegation pattern, a design pattern where an object (the delegator) forwards responsibilities to another object (the delegate). In Kotlin, this is formalized with the by keyword, which automatically generates code to delegate property access to the delegate.

For example, instead of writing:

class User {
    private var _name: String? = null
    var name: String
        get() {
            println("Getting name")
            return _name ?: "Default"
        }
        set(value) {
            println("Setting name to $value")
            _name = value
        }
}

You can delegate the logging logic to a LoggingDelegate, simplifying User:

class User {
    var name: String by LoggingDelegate("Default")
}

The LoggingDelegate encapsulates the getter/setter logic, making User focused on its core role.

Standard Delegates Provided by Kotlin

Kotlin’s standard library (kotlin.properties) includes several built-in delegates for common use cases. These are ready to use and cover most everyday needs.

Lazy Initialization (lazy)

The lazy delegate defers property initialization until the first access, making it ideal for expensive operations (e.g., network calls, database setup) that shouldn’t run until needed.

Key Features:

  • Initializes the value only once (thread-safe by default).
  • Returns the cached value on subsequent accesses.

Syntax:

val <propertyName>: <Type> by lazy(<mode>) { initializationBlock }

Modes:

  • LazyThreadSafetyMode.SYNCHRONIZED (default): Uses a lock to ensure thread safety.
  • LazyThreadSafetyMode.NONE: No synchronization (for single-threaded environments).
  • LazyThreadSafetyMode.PUBLICATION: Allows concurrent initialization but returns the first completed value.

Example:

val database: Database by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
    println("Initializing database...")
    Database.connect() // Expensive initialization
}

fun main() {
    println("Before accessing database")
    val db = database // First access: initializes
    val db2 = database // Subsequent access: uses cached value
    // Output:
    // Before accessing database
    // Initializing database...
}

Observable Properties (observable and vetoable)

These delegates track changes to a property, useful for validation, logging, or triggering side effects.

observable: Notifies when the property is updated.

Syntax:

var <propertyName>: <Type> by Delegates.observable(initialValue) { property, oldValue, newValue ->
    // Reaction to change (e.g., log, update UI)
}

Example:

import kotlin.properties.Delegates

class User {
    var age: Int by Delegates.observable(0) { prop, old, new ->
        println("${prop.name} changed from $old to $new")
    }
}

fun main() {
    val user = User()
    user.age = 25 // Output: age changed from 0 to 25
    user.age = 30 // Output: age changed from 25 to 30
}

vetoable: Blocks updates unless a condition is met (acts as a “gatekeeper”).

Syntax:

var <propertyName>: <Type> by Delegates.vetoable(initialValue) { property, oldValue, newValue ->
    // Return true to allow the change, false to veto
}

Example (prevent negative ages):

class User {
    var age: Int by Delegates.vetoable(0) { _, old, new ->
        new >= 0 // Allow only non-negative values
    }
}

fun main() {
    val user = User()
    user.age = 25 // Allowed: age becomes 25
    user.age = -5 // Vetoed: age remains 25
}

Non-Null Properties (notNull)

The notNull delegate ensures a property is initialized before use, even if declared as non-null. It throws an exception if accessed before initialization (unlike nullable var with !!).

Use Case:

Properties that can’t be initialized in the constructor (e.g., Android Activity views).

Syntax:

var <propertyName>: <Type> by Delegates.notNull()

Example:

import kotlin.properties.Delegates

class Activity {
    var textView: TextView by Delegates.notNull()

    fun onCreate() {
        textView = TextView(context) // Initialize later
    }
}

fun main() {
    val activity = Activity()
    // activity.textView // Throws IllegalStateException: Property textView should be initialized before get.
    activity.onCreate()
    activity.textView // Safe to access
}

Map-Based Properties

The map delegate stores property values in a Map (or MutableMap for var), making it easy to work with dynamic data (e.g., JSON, CSV, or configuration files with unknown fields).

Syntax:

class <ClassName>(val map: Map<String, Any>) {
    val <propertyName>: <Type> by map
}

Example (JSON-like data):

class User(map: Map<String, Any>) {
    val name: String by map // Reads map["name"]
    val age: Int by map     // Reads map["age"]
    val isStudent: Boolean by map // Reads map["isStudent"]
}

fun main() {
    val userMap = mapOf(
        "name" to "Alice",
        "age" to 22,
        "isStudent" to true
    )
    val user = User(userMap)
    println(user.name) // Alice
    println(user.age) // 22
}

For mutable properties, use MutableMap:

class MutableUser(map: MutableMap<String, Any>) {
    var name: String by map
    var age: Int by map
}

fun main() {
    val userMap = mutableMapOf("name" to "Bob", "age" to 30)
    val user = MutableUser(userMap)
    user.age = 31 // Updates userMap["age"] to 31
    println(userMap["age"]) // 31
}

Creating Custom Delegates

For unique use cases not covered by standard delegates, you can create custom delegates. A delegate is simply a class that implements getValue (and setValue for var).

Read-Only (val) Delegates

For val properties, the delegate must define a getValue method with the signature:

operator fun getValue(
    thisRef: T,  // The class instance owning the property (e.g., User)
    property: KProperty<*>  // Metadata about the property (name, type, etc.)
): R  // The property type (e.g., String)

Mutable (var) Delegates

For var properties, add a setValue method:

operator fun setValue(
    thisRef: T,
    property: KProperty<*>,
    value: R  // The new value to set
)

Practical Examples of Custom Delegates

Example 1: Logging Delegate

Track when a property is accessed or modified:

class LoggingDelegate<T>(private var initialValue: T) {
    // For val: getValue only
    operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
        println("Getting ${property.name} (value: $initialValue)")
        return initialValue
    }

    // For var: add setValue
    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
        println("Setting ${property.name} from $initialValue to $value")
        initialValue = value
    }
}

// Usage
class User {
    var name: String by LoggingDelegate("")
    val id: Int by LoggingDelegate(123)
}

fun main() {
    val user = User()
    user.name = "Alice" // Setting name from  to Alice
    println(user.name)  // Getting name (value: Alice) → Alice
    println(user.id)    // Getting id (value: 123) → 123
}

Example 2: Counter Delegate

Track how many times a property is accessed:

class CounterDelegate<T>(private val initialValue: T) {
    private var count = 0

    operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
        count++
        println("${property.name} accessed $count times")
        return initialValue
    }
}

// Usage
class Stats {
    val score: Int by CounterDelegate(100)
}

fun main() {
    val stats = Stats()
    stats.score // score accessed 1 times
    stats.score // score accessed 2 times
}

Example 3: Persistent Delegate

Save property values to a file (e.g., for simple persistence):

import java.io.File

class PersistentDelegate(private val filePath: String) {
    private var value: String? = null

    // Load from file on first access
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        if (value == null) {
            value = File(filePath).readText()
        }
        return value!!
    }

    // Save to file on update
    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        this.value = value
        File(filePath).writeText(value)
    }
}

// Usage: Persist "config.txt" content
class AppConfig {
    var apiKey: String by PersistentDelegate("config.txt")
}

fun main() {
    val config = AppConfig()
    println(config.apiKey) // Reads from config.txt
    config.apiKey = "NEW_KEY_123" // Writes to config.txt
}

Advanced Use Cases

Delegating to Another Property

You can delegate a property to another property (e.g., to expose a private property publicly):

class User {
    private var _email: String = "[email protected]"
    var publicEmail: String by ::_email // Delegate to _email
}

fun main() {
    val user = User()
    println(user.publicEmail) // [email protected]
    user.publicEmail = "[email protected]"
    println(user.publicEmail) // [email protected] (updates _email)
}

Generic Delegates with Reified Types

Use reified type parameters to create type-safe delegates without runtime overhead:

class TypeSafeDelegate<T : Any> {
    private var value: T? = null

    operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
        return value ?: error("${property.name} not initialized")
    }

    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
        this.value = value
    }
}

// Reified helper function for type inference
inline fun <reified T : Any> typeSafeDelegate() = TypeSafeDelegate<T>()

// Usage (no need to specify type explicitly)
class Example {
    var number: Int by typeSafeDelegate()
    var text: String by typeSafeDelegate()
}

Combining Delegates

Chain delegates to combine behavior (e.g., lazy + observable for lazy initialization with change tracking):

val lazyObservableValue: Int by lazy { 10 }.observable { _, old, new ->
    println("Value changed from $old to $new")
}

fun main() {
    println(lazyObservableValue) // 10 (initialized)
    lazyObservableValue = 20 // Value changed from 10 to 20 (error! lazy is read-only)
}

Note: lazy returns a Lazy<T>, which is read-only. To combine with observable, use a var and wrap lazy in a mutable delegate.

Best Practices

  1. Prefer Standard Delegates First: Use lazy, observable, etc., before writing custom delegates—they’re optimized and well-tested.
  2. Keep Delegates Stateless When Possible: Stateless delegates (no internal state) are reusable across multiple properties.
  3. Document Behavior: Clearly document custom delegates (e.g., “Thread-safe?”, “Initialization cost?”).
  4. Avoid Overuse: For simple logic (e.g., a basic getter), use a regular property instead of a delegate.
  5. Test Edge Cases: For custom delegates, test initialization, concurrency, and error conditions (e.g., notNull before initialization).

Conclusion

Kotlin’s delegated properties are a versatile tool for encapsulating property logic, reducing boilerplate, and promoting code reuse. By leveraging built-in delegates like lazy and observable, or creating custom ones for unique needs, you can write cleaner, more maintainable code.

Start with standard delegates for common tasks, and when you encounter repetitive property logic, encapsulate it in a custom delegate. With practice, delegated properties will become an indispensable part of your Kotlin toolkit.

References