Урок по треку Kotlin 6 мин

Делегаты в Kotlin за 6 минут

by lazy, observable, Delegates.notNull, делегирование класса и свои делегаты свойств.

Ключевое слово by встречается в Kotlin в двух ролях: делегирование реализации интерфейса и делегированные свойства. Первое заменяет наследование композицией, второе даёт lazy, observable, by viewModels() и by remember.

Делегирование класса

interface Repository { fun load(id: Int): String; fun save(v: String) }

class LoggingRepository(private val inner: Repository) : Repository by inner {
    override fun save(v: String) {
        println("save $v")
        inner.save(v)
    }
}

Компилятор сам сгенерирует все методы интерфейса, переадресовав их inner, а переопределить можно только нужные. Это паттерн «декоратор» без шаблонного кода и способ следовать правилу «композиция лучше наследования».

Тонкость: делегат фиксируется в момент создания объекта. Если inner объявлен как var и потом поменялся, вызовы всё равно уйдут старому объекту. И если делегат внутри себя вызывает свой же метод, переопределение в обёртке он не увидит: this у делегата это он сам, а не обёртка.

Делегированные свойства

class Profile {
    val avatar: Bitmap by lazy { loadAvatar() }
    var name: String by Delegates.observable("") { _, old, new -> println("$old -> $new") }
}

Синтаксис val x: T by d означает: геттер свойства вызывает d.getValue(this, property), а сеттер d.setValue(...). Делегатом может быть любой объект с такими операторами.

lazy

by lazy { } вычисляет значение при первом обращении и запоминает его. Режимы:

  • LazyThreadSafetyMode.SYNCHRONIZED (по умолчанию): блокировка, инициализатор выполнится ровно один раз даже при гонке потоков;
  • PUBLICATION: инициализатор может выполниться несколько раз параллельно, но сохранится первое значение;
  • NONE: без синхронизации, быстрее, только для однопоточного кода (например, главный поток Android).

Если инициализатор бросил исключение, значение не запоминается, и следующее обращение попробует снова.

lazy работает только с val. Частая ошибка в Android: val binding by lazy { ... view ... } во фрагменте, где view пересоздаётся. Ленивое значение запомнит view первого создания и будет держать его (утечка и работа со старой view).

observable и vetoable

var count: Int by Delegates.observable(0) { prop, old, new -> render() }
var age: Int by Delegates.vetoable(0) { _, _, new -> new >= 0 }  // отклонит отрицательные
var token: String by Delegates.notNull()   // как lateinit, но работает и с примитивами

Делегирование в Map

class Config(map: Map<String, Any?>) {
    val host: String by map
    val port: Int by map
}
Config(mapOf("host" to "localhost", "port" to 8080)).port  // 8080

Удобно для JSON-подобных структур, но без проверки типов при компиляции.

Свой делегат

class Trimmed : ReadWriteProperty<Any?, String> {
    private var value = ""
    override fun getValue(thisRef: Any?, property: KProperty<*>) = value
    override fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        this.value = value.trim()
    }
}

class Form { var email by Trimmed() }

Можно и без интерфейса, достаточно функций operator fun getValue и setValue. Если делегату нужно знать имя свойства при создании, реализуют provideDelegate.

Делегаты в Android

  • val vm: MainViewModel by viewModels() и by activityViewModels(): ленивое получение ViewModel из нужного ViewModelStoreOwner.
  • val args by navArgs<DetailsArgs>().
  • В Compose var text by remember { mutableStateOf("") }: State реализует getValue/setValue как расширения, поэтому работает by.
  • by preferences и подобные самописные делегаты для SharedPreferences/DataStore.

Частые ошибки

  • by lazy на значении, зависящем от жизненного цикла (view, context фрагмента).
  • Ожидать, что смена var-делегата при делегировании класса что-то изменит.
  • Использовать LazyThreadSafetyMode.NONE в многопоточном коде.
  • Путать Delegates.notNull() и lateinit: первый работает с примитивами, но без проверки isInitialized.

Что спросят на собесе

  • Как работает by lazy и какие у него режимы потокобезопасности?
  • Что такое делегирование класса и чем оно лучше наследования?
  • Как написать свой делегат свойства?
  • Как работает by viewModels()?
  • Почему by работает с mutableStateOf в Compose?

Закрепите за 5 минут

После урока в тренажёре 3-5 проверочных вопросов, дальше модуль уходит в повторения.