Delegated Properties in Kotlin

1. Introduction

The Kotlin programming language has native support for class properties.

Properties are usually backed directly by corresponding fields, but it does not always need to be like this – as long as they are correctly exposed to the outside world, they still can be considered properties.

This can be achieved by handling this in getters and setters, or by leveraging the power of Delegates.

2. What Are Delegated Properties?

Simply put, delegated properties are not backed by a class field and delegate getting and setting to another piece of code. This allows for delegated functionality to be abstracted out and shared between multiple similar properties – e.g. storing property values in a map instead of separate fields.

Delegated properties are used by declaring the property and the delegate that it uses. The by keyword indicates that the property is controlled by the provided delegate instead of its own field.

For example:

class DelegateExample(map: MutableMap<String, Any?>) {
    var name: String by map
}

This uses the fact that a MutableMap is itself a delegate, allowing you to treat its keys as properties.

3. Standard Delegated Properties

The Kotlin standard library comes with a set of standard delegates that are ready to be used.

We’ve already seen an example of using a MutableMap to back a mutable property. In the same way, you can back an immutable property using a Map – allowing individual fields to be accessed as properties, but not ever change them.

The lazy delegate allows the value of a property to be computed only on first access and then cached. This can be useful for properties that might be expensive to compute and that you might not ever need – for example, being loaded from a database:

class DatabaseBackedUser(userId: String) {
    val name: String by lazy {
        queryForValue("SELECT name FROM users WHERE userId = :userId", mapOf("userId" to userId)
    }
}

The observable delegate allows for a lambda to be triggered any time the value of the property changes, for example allowing for change notifications or updating of other related properties:

class ObservedProperty {
    var name: String by Delegates.observable("<not set>") {
        prop, old, new -> println("Old value: $old, New value: $new")
    }
}

4. Creating Your Delegates

There will be times that you want to write your delegates, rather than using ones that already exist. This relies on writing a class that extends one of two interfaces – ReadOnlyProperty or ReadWriteProperty.

Both of these interfaces define a method called getValue – which is used to supply the current value of the delegated property when it is read. This takes two arguments and returns the value of the property:

  • thisRef – a reference to the class that the property is in

  • property – a reflection description of the property being delegated

The ReadWriteProperty interface additionally defines a method called setValue that is used to update the current value of the property when it is written. This takes three arguments and has no return value:

  • thisRef – A reference to the class that the property is in

  • property – A reflection description of the property being delegated

  • value – The new value of the property

As an example, let’s write a delegate that always works regarding a database connection instead of local fields:

class DatabaseDelegate<in R, T>(readQuery: String, writeQuery: String, id: Any) : ReadWriteDelegate<R, T> {
    fun getValue(thisRef: R, property: KProperty<*>): T {
        return queryForValue(readQuery, mapOf("id" to id))
    }

    fun setValue(thisRef: R, property: KProperty<*>, value: T) {
        update(writeQuery, mapOf("id" to id, "value" to value))
    }
}

This depends on two top-level functions to access the database:

  • queryForValue – this takes some SQL and some binds and returns the first value

  • update – this takes some SQL and some binds and treats it as an UPDATE statement

We can then use this like any ordinary delegate and have our class automatically backed by the database:

class DatabaseUser(userId: String) {
    var name: String by DatabaseDelegate(
      "SELECT name FROM users WHERE userId = :id",
      "UPDATE users SET name = :value WHERE userId = :id",
      userId)
    var email: String by DatabaseDelegate(
      "SELECT email FROM users WHERE userId = :id",
      "UPDATE users SET email = :value WHERE userId = :id",
      userId)
}

5. Summary

Property delegation is a powerful technique, that allows you to write code that takes over control of other properties, and helps this logic to be easily shared amongst different classes. This allows for robust, reusable logic that looks and feels like regular property access.

A fully working example for this article can be found over on GitHub.

Leave a Reply

Your email address will not be published.