Как организована многопоточность в Jetpack Compose
Что проверяют
Понимаете ли вы, на каком потоке идёт композиция, как Compose узнаёт об изменении состояния и где запускать асинхронную работу. Следом обычно спрашивают разницу между LaunchedEffect и rememberCoroutineScope и можно ли менять состояние из фонового потока.
Как отвечать
Композиция и отрисовка идут на главном потоке: Recomposer планирует перекомпозицию по кадрам через Choreographer. При этом документация прямо предупреждает, что composable-функции могут выполняться в любом порядке, часто и потенциально параллельно, поэтому их тело должно быть без побочных эффектов.
Дальше по уровням:
- состояние:
mutableStateOfпостроен на системе снимков. Изменение значения помечает читающие его области к перекомпозиции. Писать можно и из фонового потока, но надёжнее обновлять с главного; - эффекты:
LaunchedEffect(key)запускает корутину, привязанную к композиции. Она отменяется, когда composable уходит из композиции или меняется ключ; - события:
rememberCoroutineScope()даёт scope для запуска корутины из обработчика клика, например для прокрутки списка; - мост в Flow:
snapshotFlow { listState.firstVisibleItemIndex }превращает чтения состояния в поток,produceStateделает обратное; - тяжёлая работа: во
ViewModelчерезviewModelScope, который по умолчанию работает наDispatchers.Main.immediate. Сеть и диск уходят вwithContext(Dispatchers.IO), вычисления вDispatchers.Default; - сбор данных в UI:
StateFlowиз ViewModel собирают черезcollectAsStateWithLifecycle(), чтобы подписка останавливалась в фоне.
Где спотыкаются
Считают в теле composable тяжёлое: сортируют список или парсят JSON. Это повторяется на каждой перекомпозиции и роняет частоту кадров. Место таким вычислениям во ViewModel или хотя бы в remember с ключами.
Вызывают scope.launch прямо в теле функции. Корутина запускается на каждой перекомпозиции, а если она сама меняет состояние, получается бесконечный цикл.
Грузят данные в LaunchedEffect(Unit) на экране. При повороте активити пересоздаётся, и запрос уходит снова. Первичную загрузку лучше запускать во ViewModel, которая переживает смену конфигурации.