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, is 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")
}
}
As of Kotlin 1.4, it is also possible to delegate directly to another property. For example, if we are renaming a property in an API class, we may leave the old one in place and simply delegate to the new one:
class RenamedProperty {
var newName: String = ""
@Deprecated("Use newName instead")
var name: String by this::newName
}
Here, any time we access the name property, we are effectively using the newName property instead.
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 of Kotlin 1.4, the ReadWriteProperty interface actually extends ReadOnlyProperty. This allows us to write a single delegate class implementing ReadWriteProperty and use it for read-only fields within our code. Previously, we would’ve had to write two different delegates – one for read-only fields and another for mutable fields.
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. Delegating Delegate Creation
Another new feature that we have in Kotlin 1.4 is the ability to delegate the creation of our delegate classes to another class. This works by implementing the PropertyDelegateProvider interface, which has a single method to instantiate something to use as the actual delegate.
We can use this to execute some code around the creation of the delegate to use – for example, to log what is happening. We can also use it to dynamically select the delegate that we’re going to use based on the property that it is used for. For example, we might have a different delegate if the property is nullable:
class DatabaseDelegateProvider<in R, T>(readQuery: String, writeQuery: String, id: Any)
: PropertyDelegateProvider<R, ReadWriteDelegate<R, T>> {
override operator fun provideDelegate(thisRef: T, prop: KProperty<*>): ReadWriteDelegate<R, T> {
if (prop.returnType.isMarkedNullable) {
return NullableDatabaseDelegate(readQuery, writeQuery, id)
} else {
return NonNullDatabaseDelegate(readQuery, writeQuery, id)
}
}
}
This allows us to write simpler code in each delegate because they only have to focus on more targeted cases. In the above, we know that NonNullDatabaseDelegate will only ever be used on properties that can not have a null value, so we don’t need any extra logic to handle that.
6. 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.