Состояние и эффекты это вторая половина вопросов про Compose. Нужно чётко различать remember и rememberSaveable, понимать state hoisting и знать, какой эффект брать для какой задачи.
State и remember
@Composable
fun Counter() {
var count by remember { mutableStateOf(0) }
Button(onClick = { count++ }) { Text("Нажато $count раз") }
}
mutableStateOfсоздаёт наблюдаемыйState: Compose знает, кто его прочитал, и рекомпозирует их при изменении.rememberсохраняет значение между рекомпозициями. Без негоmutableStateOf(0)создавался бы заново при каждом вызове, и счётчик всегда был бы 0.remember(key) { }пересчитывает значение при смене ключа:remember(userId) { formatter.format(userId) }.
remember живёт, пока composable находится в композиции. Ушла с экрана, вернулась, значение потеряно. При повороте тоже потеряно: Activity пересоздаётся.
rememberSaveable сохраняет значение в Bundle и переживает поворот и смерть процесса. Поддерживает типы Bundle, для своих классов нужен Saver или @Parcelize.
Для коллекций: mutableStateListOf(), mutableStateMapOf(). mutableStateOf(mutableListOf()) с изменением списка через add не вызовет рекомпозицию: ссылка та же, State не узнает об изменении.
State hoisting
Состояние поднимают к общему владельцу, а компонент делают stateless: он получает значение и колбэк.
@Composable
fun SearchField(query: String, onQueryChange: (String) -> Unit) {
TextField(value = query, onValueChange = onQueryChange)
}
@Composable
fun SearchScreen(vm: SearchViewModel = hiltViewModel()) {
val query by vm.query.collectAsStateWithLifecycle()
SearchField(query, vm::onQueryChange)
}
Плюсы: компонент переиспользуем, тестируем, единый источник правды. Правило: состояние поднимают до ближайшего общего предка всех, кто его читает или меняет, и не выше. Бизнес-состояние экрана живёт во ViewModel, чисто UI-состояние (раскрыт ли элемент, состояние скролла) может жить в composable или в state holder-классе (rememberLazyListState()).
Побочные эффекты
Тело composable должно быть чистым. Для действий во внешнем мире есть эффекты:
| API | Когда использовать |
|---|---|
LaunchedEffect(key) { } | запустить корутину при входе в композицию и перезапустить при смене ключа: загрузка, показ снекбара, анимация |
DisposableEffect(key) { onDispose { } } | подписка с обязательной отпиской: листенеры, LifecycleObserver, BroadcastReceiver |
SideEffect { } | после каждой успешной рекомпозиции синхронизировать состояние Compose с не-Compose объектом |
rememberCoroutineScope() | запустить корутину из колбэка (onClick), скоуп отменится при выходе из композиции |
rememberUpdatedState(value) | в долгоживущем эффекте всегда видеть актуальное значение, не перезапуская эффект |
produceState | превратить внешний источник в State |
snapshotFlow { } | превратить чтение State в Flow |
LaunchedEffect(userId) { // перезапуск при новом userId
viewModel.load(userId)
}
DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, e -> if (e == Lifecycle.Event.ON_RESUME) refresh() }
lifecycleOwner.lifecycle.addObserver(observer)
onDispose { lifecycleOwner.lifecycle.removeObserver(observer) }
}
Ключи эффекта определяют, когда он перезапускается. LaunchedEffect(Unit) или LaunchedEffect(true) выполнится один раз за время нахождения в композиции (но снова после поворота, ведь композиция новая). Если в ключи не включить значение, которое эффект использует, он будет работать со старым значением.
derivedStateOf
Когда состояние вычисляется из другого состояния, которое меняется чаще, чем результат:
val listState = rememberLazyListState()
val showButton by remember {
derivedStateOf { listState.firstVisibleItemIndex > 0 }
}
firstVisibleItemIndex меняется при каждой прокрутке, а showButton только дважды. Без derivedStateOf рекомпозиция шла бы на каждый шаг. Если результат меняется так же часто, как источник, derivedStateOf не нужен, хватит remember(key).
Состояние из потоков
collectAsStateWithLifecycle() собирает StateFlow/Flow в State и останавливает сбор, когда приложение уходит в фон (ниже STARTED). collectAsState() продолжает собирать и в фоне.
Частые ошибки
mutableStateOfбезremember.rememberтам, где нуженrememberSaveable(ввод пользователя).- Изменение
MutableListвнутриmutableStateOfбез замены ссылки. - Запуск корутины в теле composable вместо эффекта.
- Неверные ключи эффектов: лишние перезапуски или устаревшие значения.
- Отписка от листенера, забытая без
DisposableEffect.
Что спросят на собесе
- Чем
rememberотличается отrememberSaveable? - Что такое state hoisting и зачем он нужен?
LaunchedEffect,DisposableEffect,SideEffect,rememberCoroutineScope: когда что?- Как работают ключи эффектов?
- Когда нужен
derivedStateOf? - Чем
collectAsStateWithLifecycleлучшеcollectAsState?