Что такое derivedStateOf и когда его следует использовать?
derivedStateOf создаёт кэшированное производное значение из других State-объектов: рекомпозиция происходит только при изменении результата, а не каждый раз при изменении входных данных. Применяйте для фильтрации списков и агрегации scroll-offset, всегда вместе с remember.
Что такое derivedStateOf
derivedStateOf создаёт объект State<T>, значение которого вычисляется из других State-объектов. Compose отслеживает, какие State-объекты читаются внутри блока вычисления, и пересчитывает результат только тогда, когда результат действительно изменился — даже если входные State меняются часто.
Ключевое отличие от простого чтения State: composable перекомпозируется не при каждом изменении входных значений, а только когда изменяется результат вычисления.
Как это работает внутри
Compose использует snapshot-систему. derivedStateOf { block } создаёт DerivedSnapshotState, который запускает block при чтении и запоминает, какие State-объекты были прочитаны. При изменении любого из них вычисление запускается снова, но рекомпозиция происходит только если equals() нового и старого результата возвращает false.
Когда использовать
Главные сценарии:
- Производное булево значение из часто меняющегося State (например, кнопка активна когда форма валидна).
- Фильтрация/сортировка списка — список может обновляться часто, но результат фильтра — реже.
- Агрегация scroll-offset в флаг видимости заголовка.
@Composable
fun SearchScreen() {
val lazyListState = rememberLazyListState()
val searchQuery = remember { mutableStateOf("") }
val allItems = remember { getSampleItems() } // список из 1000 элементов
// БЕЗ derivedStateOf: фильтрация запускается при каждом scroll,
// даже когда searchQuery не изменился
// val filtered = allItems.filter { it.contains(searchQuery.value) }
// С derivedStateOf: фильтрация запускается только при изменении searchQuery
val filteredItems by remember {
derivedStateOf {
allItems.filter { it.contains(searchQuery.value, ignoreCase = true) }
}
}
// Флаг видимости кнопки «вернуться наверх»
val showScrollToTop by remember {
derivedStateOf { lazyListState.firstVisibleItemIndex > 0 }
}
Column {
TextField(
value = searchQuery.value,
onValueChange = { searchQuery.value = it },
placeholder = { Text("Search...") }
)
LazyColumn(state = lazyListState) {
items(filteredItems) { item ->
Text(item, modifier = Modifier.padding(8.dp))
}
}
AnimatedVisibility(visible = showScrollToTop) {
Button(onClick = { /* scroll to top */ }) {
Text("Наверх")
}
}
}
}
Когда derivedStateOf НЕ нужен
Если вычисление дешёвое (конкатенация строки, простое булево из одного State) — overhead от derivedStateOf превышает пользу. Также не нужен, если входные State меняются редко: обычная функция внутри composable будет проще и не хуже по производительности.
// Избыточно — простое условие не требует derivedStateOf
val isValid by remember { derivedStateOf { email.value.isNotEmpty() } }
// Достаточно читать напрямую
val isValid = email.value.isNotEmpty()
remember + derivedStateOf — обязательная пара
Всегда оборачивайте derivedStateOf в remember. Без remember новый DerivedSnapshotState создаётся при каждой рекомпозиции — вы теряете все преимущества кэширования и получаете утечку подписок.
Подводные камни
- Забыли remember — без
remember { derivedStateOf { ... } }оптимизации нет, каждая рекомпозиция создаёт новый объект. - Чтение нестабильного объекта внутри блока — если внутри блока читать обычную Kotlin-переменную (не State), изменение этой переменной не триггернёт пересчёт.
- Дорогое вычисление без оптимизации —
derivedStateOfне делает вычисление асинхронным; тяжёлые операции (сортировка O(n log n)) блокируют main thread так же, как и без него. Для тяжёлых вычислений используйтеproduceStateили Flow в ViewModel. - Несколько уровней вложенности —
derivedStateOf { derivedStateOf { ... }.value }работает, но усложняет отладку; предпочтительнее объединить в один блок. - Использование в ViewModel вместо composable —
derivedStateOfпринадлежит Compose runtime и не должен использоваться вне composable-контекста; для производных значений в ViewModel используйтеStateFlow.mapилиcombine. - Ожидание ленивого вычисления — блок вычисляется при первом чтении, а не сразу при создании; не рассчитывайте, что значение уже готово до первой рекомпозиции.
Common mistakes
- Объяснять «derivedStateOf» только как синтаксис и не описывать поведение runtime/compiler.
- Игнорировать важный риск: Использовать derivedStateOf для простого сложения строк - лишний overhead и ложная оптимизация.
- Давать пример без edge case: отмены, null, recomposition, platform boundary или ошибки.
What the interviewer is testing
- Формулирует суть темы «derivedStateOf» своими словами и связывает ее с кодом.
- Называет механизм: Он полезен для дорогих или часто меняющихся входов, когда UI должен реагировать только на агрегированный результат.
- Видит production-последствие: Использовать derivedStateOf для простого сложения строк - лишний overhead и ложная оптимизация.