Kotlin Delegated Properties: Mastering Built-in and Custom Delegation

Explore Kotlin's Delegated Properties, including built-in delegates and custom delegation behaviors. Learn how to enhance code efficiency and maintainability with practical examples and expert insights.

2.7 Delegated Properties

Delegated properties in Kotlin offer a powerful mechanism for property management, allowing developers to encapsulate common property logic in reusable components. This feature leverages the concept of delegation, where a property delegates its getter and setter logic to another object. In this section, we will delve into both built-in delegates provided by Kotlin and how to create custom delegation behaviors, enhancing your code’s efficiency and maintainability.

Introduction to Delegated Properties

Delegated properties are a unique feature of Kotlin that allows you to define a property and delegate its access and modification logic to a separate object. This delegation can be used to implement lazy properties, observable properties, and more, without cluttering your class with repetitive code.

Key Concepts

  • Delegation: The act of handing over the responsibility for a particular task to another object.
  • Property Delegate: An object that handles the logic for a property’s getter and setter.
  • Delegated Property Syntax: Using the by keyword to delegate a property.

Why Use Delegated Properties?

  1. Code Reusability: Encapsulate common property logic in reusable delegates.
  2. Separation of Concerns: Keep your classes focused on their primary responsibilities.
  3. Simplified Code: Reduce boilerplate code by using built-in delegates or creating custom ones.

Built-in Delegates in Kotlin

Kotlin provides several built-in delegates that cover common use cases, such as lazy initialization, observable properties, and more.

Lazy Initialization

The lazy delegate is used for properties that should be initialized only when they are accessed for the first time. This is particularly useful for properties that are expensive to create or compute.

1val lazyValue: String by lazy {
2    println("Computed!")
3    "Hello, Kotlin!"
4}
5
6fun main() {
7    println(lazyValue) // Prints "Computed!" followed by "Hello, Kotlin!"
8    println(lazyValue) // Prints "Hello, Kotlin!" without recomputing
9}

Key Points:

  • Thread Safety: By default, lazy is thread-safe and synchronized. You can change this behavior by specifying a different LazyThreadSafetyMode.
  • Initialization: The lambda passed to lazy is executed only once.

Observable Properties

The Delegates.observable function allows you to define a property with a callback that gets triggered whenever the property value changes.

1import kotlin.properties.Delegates
2
3var observableValue: String by Delegates.observable("Initial Value") { property, oldValue, newValue ->
4    println("${property.name} changed from $oldValue to $newValue")
5}
6
7fun main() {
8    observableValue = "New Value" // Triggers the callback
9}

Key Points:

  • Callback Parameters: The callback receives the property metadata, the old value, and the new value.
  • Use Cases: Ideal for properties where you need to perform actions on value changes, such as updating a UI.

Vetoable Properties

The Delegates.vetoable function is similar to observable, but it allows you to veto changes to the property value.

 1var vetoableValue: Int by Delegates.vetoable(0) { property, oldValue, newValue ->
 2    newValue >= 0 // Only allow non-negative values
 3}
 4
 5fun main() {
 6    vetoableValue = 10 // Accepted
 7    println(vetoableValue) // Prints 10
 8
 9    vetoableValue = -5 // Rejected
10    println(vetoableValue) // Still prints 10
11}

Key Points:

  • Veto Logic: The lambda returns a Boolean indicating whether the new value should be accepted.
  • Use Cases: Useful for properties with constraints or validation rules.

Creating Custom Delegation Behaviors

While built-in delegates cover many scenarios, there are times when you need custom behavior. Kotlin allows you to create your own property delegates by implementing the ReadOnlyProperty or ReadWriteProperty interfaces.

Custom Read-Only Delegate

A read-only delegate only requires a getValue method.

 1class UpperCaseDelegate : ReadOnlyProperty<Any?, String> {
 2    private var value: String = "default"
 3
 4    override fun getValue(thisRef: Any?, property: KProperty<*>): String {
 5        return value.toUpperCase()
 6    }
 7}
 8
 9val customValue: String by UpperCaseDelegate()
10
11fun main() {
12    println(customValue) // Prints "DEFAULT"
13}

Key Points:

  • ReadOnlyProperty Interface: Implement the getValue method to define how the property value is retrieved.

Custom Read-Write Delegate

A read-write delegate requires both getValue and setValue methods.

 1class TrimDelegate : ReadWriteProperty<Any?, String> {
 2    private var value: String = ""
 3
 4    override fun getValue(thisRef: Any?, property: KProperty<*>): String {
 5        return value
 6    }
 7
 8    override fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
 9        this.value = value.trim()
10    }
11}
12
13var trimmedValue: String by TrimDelegate()
14
15fun main() {
16    trimmedValue = "  Hello, Kotlin!  "
17    println(trimmedValue) // Prints "Hello, Kotlin!"
18}

Key Points:

  • ReadWriteProperty Interface: Implement both getValue and setValue methods to manage property access and modification.

Advanced Use Cases

Delegated properties can be used in various advanced scenarios, such as caching, logging, and more.

Caching with Delegated Properties

You can use delegated properties to implement caching mechanisms, where expensive computations are stored and reused.

 1class CacheDelegate<T>(private val computation: () -> T) : ReadOnlyProperty<Any?, T> {
 2    private var cachedValue: T? = null
 3
 4    override fun getValue(thisRef: Any?, property: KProperty<*>): T {
 5        if (cachedValue == null) {
 6            cachedValue = computation()
 7        }
 8        return cachedValue!!
 9    }
10}
11
12val expensiveValue: Int by CacheDelegate {
13    println("Computing...")
14    42
15}
16
17fun main() {
18    println(expensiveValue) // Prints "Computing..." followed by "42"
19    println(expensiveValue) // Prints "42" without recomputing
20}

Key Points:

  • Lazy Evaluation: Similar to lazy, but with custom caching logic.
  • Use Cases: Ideal for properties where the computation is costly and the result can be reused.

Logging Property Access

You can create delegates that log property access or modifications, useful for debugging or monitoring.

 1class LoggingDelegate<T>(private var value: T) : ReadWriteProperty<Any?, T> {
 2    override fun getValue(thisRef: Any?, property: KProperty<*>): T {
 3        println("Accessing ${property.name}")
 4        return value
 5    }
 6
 7    override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
 8        println("Modifying ${property.name} from $this.value to $value")
 9        this.value = value
10    }
11}
12
13var loggedValue: String by LoggingDelegate("Initial")
14
15fun main() {
16    println(loggedValue) // Logs access
17    loggedValue = "Updated" // Logs modification
18}

Key Points:

  • Monitoring: Useful for tracking property usage in complex systems.
  • Debugging: Helps identify unexpected property access patterns.

Visualizing Delegated Properties

To better understand how delegated properties work, let’s visualize the delegation process using a class diagram.

    classDiagram
	    class PropertyOwner {
	        +delegatedProperty
	    }
	    class Delegate {
	        +getValue()
	        +setValue()
	    }
	    PropertyOwner --> Delegate : "delegates to"

Diagram Description: The PropertyOwner class has a delegatedProperty that delegates its getter and setter logic to the Delegate class. This delegation is facilitated by the by keyword in Kotlin.

Design Considerations

When using delegated properties, consider the following:

  • Performance: Delegation can introduce overhead, so use it judiciously for performance-critical properties.
  • Complexity: While delegation can simplify property management, it can also add complexity if overused or misused.
  • Thread Safety: Ensure that your delegates are thread-safe if they are accessed from multiple threads.

Differences and Similarities

Delegated properties can sometimes be confused with other design patterns, such as the Proxy pattern. While both involve delegation, delegated properties are specifically for property management, whereas the Proxy pattern is a structural design pattern for controlling access to objects.

Try It Yourself

To deepen your understanding of delegated properties, try modifying the examples provided:

  • Experiment with Lazy Initialization: Change the thread safety mode of the lazy delegate and observe the behavior.
  • Create a Custom Delegate: Implement a delegate that logs both access and modification times for a property.
  • Combine Delegates: Use multiple delegates in a single class to manage different properties with distinct behaviors.

Knowledge Check

Before moving on, consider these questions:

  • How does the lazy delegate differ from a custom caching delegate?
  • What are the advantages of using Delegates.observable over manually implementing change listeners?
  • When would you choose to use a vetoable property?

Embrace the Journey

Remember, mastering delegated properties is just one step in becoming proficient with Kotlin. As you continue to explore and experiment, you’ll discover new ways to leverage Kotlin’s powerful features to write clean, efficient, and maintainable code. Keep pushing the boundaries, stay curious, and enjoy the journey!

Quiz Time!

Loading quiz…
Revised on Thursday, April 23, 2026