Jetpack ComposeMiddleTechnical

Что такое 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 вместо composablederivedStateOf принадлежит 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 и ложная оптимизация.

Sources

Related topics