Property delegation
This is a chapter from the book Advanced Kotlin. You can find it on LeanPub.
In programming, there are a number of patterns around properties, like lazy property, property value injection, property binding, etc. In most languages, there is no easy way to extract such patterns, and as a consequence, 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 the feature that is (currently) unique to Kotlin: Property Delegation. The trademark of this feature is the by
keyword after a property definition, after which a delegate is specified. Here is an example usage of property delegation, together with functions from Kotlin stdlib, we use to implement lazy or observable properties:
Later in this chapter we will discuss in detail both lazy
and observable
functions. 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 could implement our own lazy in a few simple lines of code. We can also implement our own delegates. Many Kotlin libraries already use this possibility. 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 those two properties is nearly identical. This common behavior 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. Those functions can be delegated into methods of another object. The getter will be delegated to the getValue
function and the setter to the setValue
function. An object with those 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 that, let's see under the hood and check out what is our Kotlin property delegate usage 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 it step by step. When you get a property value, you are calling its getter. Property delegation delegates this getter to the getValue
function. When you set a property value, you are calling its setter. Property delegation delegates this setter to the setValue
function. This way, each delegate fully controls property behavior.
Other getValue
and setVale
parameters
You might also have noticed that the getValue
and setValue
methods operate not only on the value 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.
KProperty
type will be better covered in the later chapter Reflection.
When we have multiple getValue
and setValue
methods but with different context types, different ones 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 with each of them, it should behave differently based on what is offered by the context:
Implementing 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 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 or supertype of the type used by the property. getValue
result type should be the same or a subtype of the type used by the property.
Those 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 following extension function 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 interface ReadOnlyProperty
(when we define a property delegate for val
) or ReadWriteProperty
(when we define a property delegate for var
) from Kotlin stdlib. Those 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. The interfaceReadOnlyProperty
uses the type parameterV
only at out-positions, so it has the covariantout
modifier.
Both ReadOnlyProperty
and ReadWriteProperty
require two type arguments. The first one is for receiver type, and it is typically Any?
that allows our property delegate to be used in every context. The second one should be the property value type. If we define a property delegate for properties of a certain type, we set this type here. If we want it generic, we use the type parameter as this type argument position.
Provide delegate
This is a story as old as time. You delegate a task to another person, who, instead of doing that, delegates it to someone else. Objects in Kotlin can do the same. An object can define the method provideDelegate
, 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 operate on values stored in preference files more easily[031_2]. This is how you want your library to be used:
The function bindToPreference
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, but because delegates are used, some additional operations might be made, like those property values might be bound 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 not be used as delegates themselves.
We can also use the PropertyDelegateProvider
interface, which specifies the provideDelegate
function with appropriate arguments and result types. Two type parameters of PropertyDelegateProvider
represent the receiver reference type and provided property delegate type.
Personally, I am not a fan of using raw values as delegates, 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 one after another and present their use cases.
notNull
delegate
I will start with the simplest property delegate, which is created using the notNull
method from the Delegates
object defined in Kotlin stdlib. It is an alternative to lateinit
, so the property delegated to notNull
behaves like a regular property, but it has no initial value, so if you try to get a value before setting one, it will result with an exception.
Wherever possible, we should prefer to use the lateinit property instead of the notNull
delegate because of performance. Lateinit properties are faster. However, currently, 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 for properties whose values are injected or as part of DSL builders.
In the next part of this series, you will see other property delegates from Kotlin stdlib, together with their use cases. So stay tuned.
The below 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 chapter Classes.
Before you do that, consider the fact that there are already many such libraries, like PreferenceHolder I published years ago.