State<List<User>?> не делегат: топ 3 ошибки и решения в Kotlin

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Тихая паника: когда Kotlin кричит, что State не может быть делегатом

Вы пишете код на Kotlin, используете Jetpack Compose, всё идет по плану. И вдруг — бац! — компилятор выдает сообщение, от которого у новичка холодеет в животе: Type 'State<List<User>?>' has no method 'getValue(Nothing?, KProperty<*>)' and thus it cannot serve as a delegate. Звучит как приговор, но на самом деле это просто крик о помощи от вашего кода. Давайте разберемся, что он хочет сказать и как с ним подружиться.

Эта ошибка — одна из самых популярных на Stack Overflow (тысячи просмотров!), и она преследует разработчиков, которые только начинают осваивать делегированные свойства в Kotlin. В этой статье мы не просто переведем сообщение об ошибке, а покажем три реальные ситуации, когда она возникает, и дадим рабочие решения. Вы узнаете, как правильно использовать StateFlow в Compose, как не запутаться в Kotlin делегатах и почему LiveData иногда ведет себя не так, как ожидается.

Что такое делегат в Kotlin и почему State им не становится?

Для начала — быстрый ликбез. Делегированные свойства в Kotlin (ключевое слово by) позволяют перенести логику геттера и сеттера в отдельный объект. Например:

var name: String by Delegates.observable("") { prop, old, new ->
    println("$old → $new")
}

Чтобы объект мог быть делегатом, он должен реализовать методы getValue() и, возможно, setValue() с определенной сигнатурой. Ошибка Type 'State<List<User>?>' has no method 'getValue(Nothing?, KProperty<*>)' говорит именно об этом: ваш объект типа State<List<User>?> не имеет подходящего метода getValue. Компилятор не может использовать его как делегат.

Теперь перейдем к трём конкретным сценариям, где эта ошибка всплывает чаще всего.

Ошибка 1: Путаница с by и присваиванием StateFlow

Самый частый случай — когда разработчик пытается использовать by для присваивания StateFlow в ViewModel, но делает это неправильно. Посмотрите на этот код:

// ❌ Ошибка! Так делать нельзя
class MyViewModel : ViewModel() {
    private val _users = MutableStateFlow<List<User>?>(null)
    val users: StateFlow<List<User>?> by _users  // Ошибка!
}

Здесь _users — это MutableStateFlow, а мы пытаемся сделать его делегатом для свойства users. Но StateFlow не реализует getValue для Kotlin-свойств. Это не делегат, это реактивный поток.

Решение: используйте asStateFlow()

Правильный способ — не делегировать, а преобразовывать:

// ✅ Правильно
class MyViewModel : ViewModel() {
    private val _users = MutableStateFlow<List<User>?>(null)
    val users: StateFlow<List<User>?> = _users.asStateFlow()
}

Метод asStateFlow() создает неизменяемую копию, которую безопасно выставлять наружу. Никаких делегатов, никаких ошибок.

Ошибка 2: Использование by с LiveData в Compose

Вторая по популярности ситуация — попытка использовать LiveData напрямую с by в Jetpack Compose. Разработчики, пришедшие из мира View-систем, привыкли к LiveData и пытаются применить тот же подход:

// ❌ Ошибка!
@Composable
fun UserList(viewModel: MyViewModel) {
    val users: List<User>? by viewModel.users  // Ошибка!
    // ...
}

Здесь viewModel.users имеет тип LiveData<List<User>?>. А LiveData не является делегатом для Compose-свойств. Компилятор снова выдает ту же ошибку: Type 'LiveData<List<User>?>' has no method 'getValue'.

Решение: используйте observeAsState() или collectAsState()

В Compose нужно преобразовывать LiveData в State с помощью расширения observeAsState():

// ✅ Правильно
@Composable
fun UserList(viewModel: MyViewModel) {
    val users by viewModel.users.observeAsState()
    // ...
}

Для StateFlow используется collectAsState():

// ✅ Для StateFlow
@Composable
fun UserList(viewModel: MyViewModel) {
    val users by viewModel.users.collectAsState()
    // ...
}

Обратите внимание: collectAsState() и observeAsState() возвращают State, который уже является делегатом. Поэтому by работает корректно.

Ошибка 3: Неправильный импорт или кастомный класс State

Третья причина — вы случайно используете свой собственный класс с именем State или импортируете не тот класс. Например, если у вас есть класс:

data class State<T>(val value: T)

И вы пытаетесь сделать его делегатом:

// ❌ Ошибка!
val users: List<User>? by State(null)  // Ошибка!

Ваш State не реализует getValue. Даже если он называется так же, как androidx.compose.runtime.State, это не делает его делегатом.

Решение: реализуйте интерфейс ReadOnlyProperty

Если вы действительно хотите создать свой делегат, нужно реализовать интерфейс ReadOnlyProperty или ReadWriteProperty из стандартной библиотеки:

// ✅ Кастомный делегат
class MyState<T>(private val initialValue: T) : ReadOnlyProperty<Any?, T> {
    private var value = initialValue

    override fun getValue(thisRef: Any?, property: KProperty<*>): T {
        return value
    }

    fun update(newValue: T) {
        value = newValue
    }
}

// Использование
val users by MyState<List<User>?>(null)

Но в 99% случаев вам это не нужно. Используйте готовые решения из Compose или корутин.

Как отличить делегата от не-делегата: шпаргалка

Чтобы больше никогда не попадаться на эту ошибку, запомните простое правило:

  • StateFlow, LiveData, MutableStateFlow — НЕ являются деле

Блоги

Рекомендаций по книгам