Ввод в поле поиска подтормаживает — найдите лишнюю рекомпозицию и уберите её
Этот экран показывает длинный список заказов и поле поиска. Сам список скроллится плавно, но каждое нажатие клавиши в поле роняет кадры — и чем длиннее список, тем хуже. Счётчики рекомпозиций показывают, что OrderScreen рекомпозируется на каждый символ.
Ограничения: сохранить тот же результат (сортировка, затем фильтр), не переносить данные в ViewModel и не уводить работу на другой поток — чините саму композицию.
@Composable
fun OrderScreen(vm: OrderViewModel) {
val orders by vm.orders.collectAsState()
var query by remember { mutableStateOf("") }
val sorted = orders.sortedWith(orderComparator) // тяжело для длинного списка
Column {
SearchField(value = query, onValueChange = { query = it })
LazyColumn {
items(sorted.filter { it.title.contains(query, ignoreCase = true) }) { OrderRow(it) }
}
}
}
Найдите и исправьте ошибку.
Чтение query прямо в OrderScreen рекомпозирует всё его тело на каждое нажатие, а sorted — обычная локальная переменная, поэтому список пересортировывается заново, хотя orders не менялся. Закэшируйте сортировку через remember(orders), а query читайте в наименьшем composable, которому он нужен.
- ✗Считать, что вычисление внутри composable выполняется один раз, а не на каждой рекомпозиции
- ✗Читать состояние на верху экрана и ждать, что рекомпозируется только использующий его потомок
- ✗Считать рекомпозиции бесплатными и смотреть только на стоимость отрисовки кадра
- →Когда нужен
derivedStateOf, а не обычныйremember(key)? - →Какой инструмент даёт счётчик рекомпозиций по composable и о чём говорит большое число пропусков?
Баг
Каждое нажатие клавиши пишет в query. Это состояние читается прямо в OrderScreen, поэтому рекомпозируется всё тело экрана — а значит, заново выполняется и orders.sortedWith(...). Обычная локальная переменная ничего не кэширует между рекомпозициями, так что длинный список пересортировывается на каждый символ, хотя сам orders не менялся.
var query by remember { mutableStateOf("") }
val sorted = orders.sortedWith(orderComparator) // ❌ пересчёт на каждую рекомпозицию
Исправление
Закэшируйте сортировку через remember с ключом по входу — она пойдёт только при новом orders. Затем сузьте чтение состояния: пусть query живёт в том composable, который его показывает, а список получает готовое значение.
val sorted = remember(orders) { orders.sortedWith(orderComparator) } // ✅ только при новом orders
val visible by remember {
derivedStateOf { sorted.filter { it.title.contains(query, ignoreCase = true) } }
}
Проверяйте результат счётчиком рекомпозиций (Layout Inspector или трассировка композиции): у OrderScreen число рекомпозиций на символ должно упасть, а тяжёлая работа — исчезнуть из кадра.
Ловушка
remember(orders) без ключа кэширует навсегда и покажет устаревший список; remember с ключом query вернёт нас к пересчёту на каждый символ. Ключ — это ровно вход вычисления.