Delegates and delegated properties in Kotlin
Today we will dive into the world of delegates and delegated properties in Kotlin. This topic may seem complicated at first glance, but I will try to explain it as clearly and in detail as possible. So let's get started!
What are delegates?
Before going into details, let's understand the basic concepts.
Delegate is an object to which another object delegates the right to perform a certain task. In Kotlin, delegation is a powerful tool that allows you to reuse code and implement complex behavior without the need for inheritance.
Delegation example
Let's look at a simple example:
Delegation example
interface Base {
fun print()
}
class BaseImpl(private val x: Int) : Base {
override fun print() {
println(x)
}
}
class Derived(b: Base) : Base by b
fun main() {
val b = BaseImpl(10)
Derived(b).print() // Выведет: 10
}
In this example:
Interface is defined
Base
with methodprint()
.Class
BaseImpl
implements this interface and stores the valuex
.Class
Derived
delegates the implementation of the interfaceBase
objectb
.
When we call print()
on a copy Derived
the method is actually executed print()
object BaseImpl
. This allows you to reuse code and add flexibility to the application architecture without excessive inheritance.
Delegated Properties
Now let's look at delegated properties
A delegated property is a property that passes its getters and setters to another object. The syntax for declaring a delegated property is as follows:
class Example {
var p: String by Delegate()
}
Here p
– delegated property. All requests to p
will be redirected to the object Delegate()
.
Built-in Kotlin delegates
Kotlin provides several built-in delegates that make working with common tasks easier.
lazy – lazy initialization
Delegate lazy
used for lazy property initialization. The value is calculated only the first time it is accessed.
val lazyValue: String by lazy {
println("Вычисляем значение...")
"Привет"
}
fun main() {
println(lazyValue) // Выведет: Вычисляем значение... Привет
println(lazyValue) // Выведет: Привет
}
In this example, the first time you access lazyValue
a block of code is executed inside lazy
and the value is saved for later use.
observable – observable property
Delegate observable
allows you to track property changes and respond to them.
import kotlin.properties.Delegates
var name: String by Delegates.observable("Начальное значение") { prop, old, new ->
println("$old -> $new")
}
fun main() {
name = "Первое" // Выведет: Начальное значение -> Первое
name = "Второе" // Выведет: Первое -> Второе
}
Here with every change name
a block of code is executed that prints the old and new values.
Creating your own delegates
In addition to the built-in ones, we can create our own delegates to implement specific behavior. Interfaces are used for this ReadOnlyProperty
And ReadWriteProperty
or you can directly implement the operators getValue
And setValue
.
Using ReadOnlyProperty
Let's create a delegate that always returns the same value and logs each call.
ReadOnlyProperty implementation example
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty
class ConstantValue<T>(private val value: T) : ReadOnlyProperty<Any?, T> {
override fun getValue(thisRef: Any?, property: KProperty<*>): T {
println("Получение значения свойства '${property.name}'")
return value
}
}
class Example {
val constant: String by ConstantValue("Hello, World!")
}
fun main() {
val example = Example()
println(example.constant)
// Выведет:
// Получение значения свойства 'constant'
// Hello, World!
}
Using ReadWriteProperty
Now let's create a delegate that logs property read and write operations.
Example implementation of ReadWriteProperty
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
class LoggingProperty<T>(private var value: T) : ReadWriteProperty<Any?, T> {
override fun getValue(thisRef: Any?, property: KProperty<*>): T {
println("Получение значения свойства '${property.name}': $value")
return value
}
override fun setValue(thisRef: Any?, property: KProperty<*>, newValue: T) {
println("Изменение значения свойства '${property.name}' на $newValue")
value = newValue
}
}
class Example {
var logged: String by LoggingProperty("Начальное")
}
fun main() {
val example = Example()
println(example.logged)
example.logged = "Новое значение"
println(example.logged)
// Выведет:
// Получение значения свойства 'logged': Начальное
// Начальное
// Изменение значения свойства 'logged' на Новое значение
// Получение значения свойства 'logged': Новое значение
// Новое значение
}
Usage ReadOnlyProperty
And ReadWriteProperty
allows you to explicitly specify which operations the delegate supports, making your code more readable and understandable.
Direct implementation of getValue and setValue
Alternatively, we can directly implement the operators getValue
And setValue
.
Example implementation of getValue/setValue
import kotlin.reflect.KProperty
class StringDelegate {
private var value: String = ""
operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
println("Получение значения свойства '${property.name}'")
return value
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, newValue: String) {
println("Изменение значения свойства '${property.name}' на $newValue")
value = newValue
}
}
class Example {
var str: String by StringDelegate()
}
fun main() {
val example = Example()
example.str = "Привет"
println(example.str)
// Выведет:
// Изменение значения свойства 'str' на Привет
// Получение значения свойства 'str'
// Привет
}
This approach gives more flexibility, allowing us to implement only the necessary methods for our purposes.
Practical Applications of Delegates
Let's look at a few practical scenarios where delegates can be especially useful.
Lazy initialization of resource-intensive objects
Delegate lazy
Excellent for lazy initialization of objects whose creation requires significant resources.
Example of using lazy
class ResourceManager {
val database by lazy {
println("Подключение к базе данных...")
Database.connect()
}
}
fun main() {
val manager = ResourceManager()
println("ResourceManager создан")
// База данных ещё не инициализирована
manager.database.query("SELECT * FROM users")
// База данных уже инициализирована
manager.database.query("SELECT * FROM products")
}
In this example, the connection to the database occurs only on the first call to database
saving resources until the moment when they are really needed.
Implementation of the “Observer” pattern
Using a delegate observable
You can easily implement the Observer pattern by monitoring property changes.
Observable usage example
import kotlin.properties.Delegates
class User {
var name: String by Delegates.observable("") { prop, old, new ->
println("Имя пользователя изменилось с '$old' на '$new'")
}
}
fun main() {
val user = User()
user.name = "Алиса" // Выведет: Имя пользователя изменилось с '' на 'Алиса'
user.name = "Боб" // Выведет: Имя пользователя изменилось с 'Алиса' на 'Боб'
}
This allows you to perform certain actions when properties change, such as updating the interface, validating data, or sending notifications.
Conclusion
Delegates are a powerful tool in a Kotlin developer's arsenal. They allow you to write cleaner, more modular and flexible code, opening up new possibilities for implementing complex logic and design patterns.
I hope this tutorial has helped you better understand delegates and delegated properties in Kotlin. Feel free to experiment and apply these concepts to your projects!
PS
I will be glad to see everyone on my Telegram channel