Delegates and delegated properties in Kotlin

Android Insights

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 method print().

  • Class BaseImpl implements this interface and stores the value x.

  • Class Derived delegates the implementation of the interface Base object b.

When we call print() on a copy Derivedthe 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 lazyand 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 ReadWritePropertyor 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 databasesaving 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

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *