Property delegation
This is a chapter from the book Advanced Kotlin. You can find it on LeanPub or Amazon.
In programming, there are a number of patterns that use properties, like lazy properties, property value injection, property binding, etc. In most languages, there is no easy way to extract such patterns, therefore developers tend to repeat the same patterns again and again, or they need to depend on complex libraries. In Kotlin, we extract repeatable property patterns using a feature that is (currently) unique to Kotlin: property delegation. This feature’s trademark is the by
keyword, which is used between a property definition and a delegate specification. Together with some functions from Kotlin stdlib, here is an example usage of property delegation that we use to implement lazy or observable properties:
Later in this chapter, we will discuss both lazy
and observable
functions in detail. For now, all you need to know is that delegates are just functions; they are not Kotlin keywords (just like lazy
in Scala or Swift). We can implement our own lazy function in a few simple lines of code. Just like many Kotlin libraries, we can also implement our own property delegates. Some good examples are View and Resource Binding, Dependency Injection0, and Data Binding.
How property delegation works
To understand how we can extract other common behaviors using property delegation, let’s start with a very simple property delegate. Let’s say we need to define properties with custom getters and setters that print their value changes1:
Even though token
and attempts
are of different types, the behavior of these two properties is nearly identical and can be extracted using property delegation.
Property delegation is based on the idea that a property is defined by its accessors: for val
, it is a getter; for var
, it is a getter and a setter. These functions can be delegated to another object’s methods:the getter will be delegated to the getValue
function, and the setter to the setValue
function. An object with these methods needs to be created and placed on the right side of the by
keyword. To make our properties behave the same way as in the example above, we can create the following delegate:
To fully understand how property delegation works, take a look at what by
is compiled to. The above token
property will be compiled to something similar to the following code:
To make sure we understand this, let's look under the hood and check out what our Kotlin property delegate usage is compiled to.
// Java representation of this code:
@Nullable
private static final LoggingProperty token$delegate =
new LoggingProperty((Object)null);
@Nullable
public static final String getToken() {
return (String)token$delegate
.getValue((Object)null, $$delegatedProperties[0]);
}
public static final void setToken(@Nullable String var0) {
token$delegate
.setValue((Object)null, $$delegatedProperties[0], var0);
}
public static final void main() {
setToken("AAA");
String res = getToken();
System.out.println(res);
}
Let's analyze this step by step. When you get a property value, you call this property's getter; property delegation delegates this getter to the getValue
function. When you set a property value, you are calling this property's setter; property delegation delegates this setter to the setValue
function. This way, each delegate fully controls this property’s behavior.
Other getValue
and setValue
parameters
You might also have noticed that the getValue
and setValue
methods not only receive the value that was set to the property and decide what its getter returns, but they also receive a bounded reference to the property as well as a context (this
). The reference to the property is most often used to get its name and sometimes to get information about annotations. The parameter referencing the receiver gives us information about where the function is used and who can use it.
The
KProperty
type will be better covered later in the Reflection chapter.
When we have multiple getValue
and setValue
methods but with different context types, different definitions of the same method will be chosen in different situations. This fact can be used in clever ways. For instance, we might need a delegate that can be used in different kinds of views, but it should behave differently with each of them based on what is offered by the context:
Implementing a custom property delegate
To make it possible to use an object as a property delegate, all it needs is the getValue
operator for val
and the getValue
and setValue
operators for var
. Both getValue
and setValue
are operators, so they need the operator
modifier. They need parameters for thisRef
of any type (most likely Any?
) and property
of type KProperty<*>
. setValue
should additionally have a property for a value whose type should be the same type or a supertype of the type used by the property. The getValue
result type should be the same type or a subtype of the type used by the property.
These methods can be member functions, but they can also be extension functions. For instance, Map<String, *>
can be used as a property delegate thanks to the extension function below, which is defined in the standard library. We will discuss using Map<String, *>
as a delegate later in this chapter.
When we define a delegate, it might be helpful to implement the ReadOnlyProperty
interface (when we define a property delegate for val
) or the ReadWriteProperty
interface (when we define a property delegate for var
) from Kotlin stdlib. These interfaces specify getValue
and setValue
with the correct parameters.
Notice how those interfaces use generic variance modifiers. Type parameter
T
is only used in in-positions, so it has the contravariantin
modifier. TheReadOnlyProperty
interface uses the type parameterV
only in out-positions, so it has the covariantout
modifier.
Both ReadOnlyProperty
and ReadWriteProperty
require two type arguments. The first is for the receiver type and is typically Any?
, which allows our property delegate to be used in any context. The second argument should be the property's value type. If we define a property delegate for properties of a certain type, we set this type here. We can also use a generic type parameter in this type argument position.
Provide a delegate
This is a story as old as time. You delegate a task to another person, who, instead of doing it, delegates it to someone else. Objects in Kotlin can do the same. An object can define the provideDelegate
method, which returns another object that will be used as a delegate.
The power of provideDelegate
is that the object that is used on the right side of the by
keyword does not need to be used as a delegate. So, for instance, an immutable object can provide a mutable delegate.
Let's see an example. Let's say you implement a library to make it easier to operate on values stored in preference files2. This is how you want your library to be used:
The bindToPreference
function returns an object that can be used as a property delegate. But what if we want to make it possible to use Int
or Boolean
as delegates in the scope of PreferenceHolder
?
This way, using delegates is similar to assigning some values; however, because delegates are used, some additional operations might be performed, such as binding these property values to preference file values.
It is problematic to use Int
or Boolean
as delegates because they are not implemented to be used this way. We could implement getValue
and setValue
to operate on preference files, but it would be harder to cache values. The simple solution is to define the provideDelegate
extension function on Int
and Boolean
. This way, Int
or Boolean
can be used on the right side of by
, but they can’t be used as delegates themselves.
We can also use the PropertyDelegateProvider
interface, which specifies the provideDelegate
function with appropriate arguments and result types. The two type parameters of PropertyDelegateProvider
represent the receiver reference type and the type of the property we delegate.
Personally, I am not a fan of using raw values as delegates because I believe that additional function names improve readability. However, this is the best example of using
provideDelegate
I could find.
Property delegates in Kotlin stdlib
Kotlin provides the following standard property delegates:
Delegates.notNull
lazy
Delegates.observable
Delegates.vetoable
Map<String, T>
andMutableMap<String, T>
Let's discuss them individually and present their use cases.
The notNull
delegate
I will start with the simplest property delegate, which is created using the notNull
method from the Delegates
object that is defined in Kotlin stdlib. It is an alternative to lateinit
, so the property delegated to notNull
behaves like a regular property but has no initial value. Therefore, if you try to get a value before setting it, this results in an exception.
Wherever possible, we should use the lateinit property instead of the notNull
delegate for better performance because lateinit properties are faster. Currently, however, Kotlin does not support lateinit properties with types that associate with primitives, like Int
or Boolean
.
In such cases, we use the notNull
delegate.
I often see this delegate used as part of DSL builders or for properties whose values are injected.
In the next part of this series, you will see other property delegates from Kotlin stdlib, together with their use cases. So, stay tuned!
This example use of Koin formally presents service location, not dependency injection.
I assume you are familiar with custom getters and setters. I explain them in the previous book from this series, Kotlin Essentials, in the Classes chapter.
Before you do this, consider the fact that there are already many similar libraries, such as PreferenceHolder, which I published years ago.