In addition to class delegates, Kotlin has a robust functionality known as delegated properties. This feature allows the delegation of getter and setter methods of a property to another object. This capability proves valuable when it comes to reusing common behavior, separating logic, or calculating a value only once. While the details of creating and utilizing delegated properties are covered in a separate topic, we will provide a concise overview here. We will also explore the standard delegated properties in Kotlin.
Overview of delegated properties
Unlike traditional properties, delegated properties are not backed by a class field. Instead, they delegate getting and setting to another piece of code. This abstraction allows for shared functionality between similar properties. For instance, you can store property values in a map instead of using 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 Example {
var p: String by Delegate()
}
The syntax is as follows: val/var <property name>: <Type> by <delegate>.
Under the hood, the delegate has the getValue() and setValue() methods, which take over the get() and set() methods, respectively, corresponding to the property.
Standard delegates
The Kotlin standard library comes with a set of standard delegates that are ready to be used in order to create:
- Lazy properties: the value is computed only during the first access.
- Observable properties: listeners are notified about changes to this property.
- Vetoable properties: allowing a lambda function to decide if a new value should be accepted or rejected.
- NotNull properties: a property delegate for a non-null property that must be initialized before it is accessed.
- Storing properties in a map: instead of using a separate field for each property, properties can be stored in a map.
Let's further explore each one of them.
1) Lazy properties: The lazy function takes a lambda and returns an instance of Lazy<T>, which serves as a delegate for implementing a lazy property. The first call to get() executes the lambda passed to lazy() and remembers the result. Subsequent calls to get() simply return the remembered (cached) result.
Let's take an example:
val lazyValue: String by lazy {
print("Computed! ")
"Hello"
}
fun main() {
println(lazyValue) // Computed! Hello
println(lazyValue) // Hello
}
In this example, the code inside the lambda gets executed only during the first access to the lazyValue property, and the result will be cached. After that, any additional access to this value will only retrieve the cached value without executing any code, so the second println() will only print the value "Hello".
2) Observable properties: The observable delegate allows for a lambda to be triggered any time the value of the property changes, resulting in change notifications or updating of other related properties.
Let's look at an example:
import kotlin.properties.Delegates
class User {
var rank: String by Delegates.observable("<no rank>") {
prop, old, new -> println("${prop.name}: $old -> $new")
}
}
fun main() {
val user = User()
user.rank = "first" // rank: <no rank> -> first
user.rank = "second" // rank: first -> second
}
In this example, the User class has the rank property, which is delegated to an observable delegate. The observable delegate takes two arguments: the initial value of the property ("<no rank>" in this case) and a lambda function that is called whenever the property is changed.
The lambda function takes three arguments: a reference to the property being changed (prop), the old value of the property (old), and the new value of the property (new). In this example, the lambda function simply prints the name of the rank property followed by its old and new values whenever it is changed.
observable, vetoable, or notNull delegates in your code, you must import the kotlin.properties.Delegates package.3) Vetoable properties: The vetoable delegate works similarly to the observable delegate, but with some key differences. The lambda function is called before a new value is set, and it allows the function to decide if the new value should be accepted or rejected.
Let's consider an example:
import kotlin.properties.Delegates
var max: Int by Delegates.vetoable(0) { prop, old, new ->
new > old
}
fun main() {
println(max) // 0
max = 10
println(max) // 10
max = 5
println(max) // 10
}
In this example, the vetoable delegate ensures that max can only be set to a value greater than its current value. If an attempt is made to set it to a lower value, the new value is discarded and the property retains the old one.
4) NotNull properties: notNull is a property delegate for a non-null property that must be initialized before it is accessed.
Let's take an example:
import kotlin.properties.Delegates
class Person{
var name: String by Delegates.notNull()
}
fun main() {
val person = Person()
person.name // Throws IllegalStateException:
// Property name should be initialized before get.
person.name = "Ahmed Omar"
println(person.name) // Prints "Ahmed Omar"
}
In this example, the notNull delegate ensures that the name property of the Person class is initialized before it is accessed. If an attempt is made to access it before it is initialized, an exception is thrown.
5) Storing properties in a map: Properties can be stored in a MutableMap or Map to back mutable or immutable properties, respectively.
Look at the following example:
class User(val map: MutableMap<String, Any?>) {
var name: String by map
var age: Int by map
}
fun main() {
val user = User(mutableMapOf(
"name" to "Ahmed Omar",
"age" to 25
))
println(user.name) // Prints "Ahmed Omar"
println(user.age) // Prints 25
user.name = "Ahmed Omar"
user.age = 30
println(user.name) // Prints "Ahmed Omar"
println(user.age) // Prints 30
}
In this example, the User class is defined, which takes a MutableMap as a constructor parameter. The name and age properties of the User class are delegated to the map property, which means that their values are stored in the map instead of separate fields for each property.
Practical use cases
Standard delegated properties in Kotlin can be utilized in various practical scenarios. Some of those include:
- Lazy initialization: The
lazydelegate can be used to initialize a property only when it is first accessed; it is useful for properties that are expensive to compute or that might not be needed at all. - Observing property changes: The
observabledelegate can be used to observe changes to a property and perform actions when the property is changed, such as updating the UI or validating the new value. - Vetoing property changes: The
vetoabledelegate can be used to veto changes to a property based on custom logic, such as ensuring that a value remains within a certain range or meets certain criteria. - NotNull properties: The
notNulldelegate can be used in situations where a property must be initialized before it is accessed and where accessing it before it is initialized should result in an exception being thrown. This can be useful for ensuring that certain preconditions are met before an object is used. - Storing properties in a map: The
mapandmutableMapdelegates can be used to store properties in a map instead of separate fields for each property; they are useful for dynamic data structures or for serializing and deserializing objects.
Conclusion
Standard delegated properties in Kotlin provide a flexible and efficient way to implement common behavior for properties. They can simplify code, improve composition and modularity, and provide flexibility to change the behavior of objects, thus enhancing the overall efficiency and readability of your Kotlin code.