ViewModel, StateFlow и collectAsState
Выносим состояние экрана в ViewModel и подписываем UI на него через StateFlow и collectAsState.
ViewModel хранит состояние и логику экрана и переживает пересоздание UI (например, при повороте), а StateFlow отдаёт это состояние наблюдаемым потоком.
Зачем выносить состояние из composable
Состояние через remember живёт в композиции и теряется, когда Activity пересоздаётся (поворот экрана, нехватка памяти). Для важных данных это плохо. ViewModel существует дольше: она переживает такие пересоздания и держит состояние и бизнес-логику отдельно от UI.
class CounterViewModel : ViewModel() {
private val _count = MutableStateFlow(0)
val count: StateFlow<Int> = _count
fun increment() {
_count.value += 1
}
}Здесь MutableStateFlow — изменяемый поток с текущим значением. Наружу отдаётся только read-only StateFlow, чтобы UI не менял состояние напрямую — он лишь зовёт методы вроде increment().
Подписка из UI
Чтобы UI реагировал на поток, его превращают в Compose-состояние через collectAsState:
@Composable
fun CounterScreen(viewModel: CounterViewModel = viewModel()) {
val count by viewModel.count.collectAsState()
Button(onClick = { viewModel.increment() }) {
Text(text = "Счёт: $count")
}
}collectAsState() подписывается на StateFlow и возвращает обычное Compose-состояние. Когда поток испускает новое значение, count обновляется, и Compose перерисовывает кнопку. Функция viewModel() даёт экземпляр ViewModel, привязанный к экрану. На реальных экранах удобно собирать всё состояние в один data class (так называемый UiState) и отдавать его единым StateFlow — тогда UI подписывается на один объект, а не на десяток отдельных потоков. Для безопасного сбора с учётом жизненного цикла в продакшене берут collectAsStateWithLifecycle(), чтобы подписка приостанавливалась, когда экран не виден.
Схема потока данных
UI --increment()--> ViewModel --обновляет--> StateFlow ^ | +------ collectAsState() (новое значение) -----+
Как работает под капотом
ViewModel хранится в специальном контейнере, привязанном к Activity/экрану, и не уничтожается при простом пересоздании UI. StateFlow всегда имеет «текущее значение» и при подписке сразу отдаёт его. collectAsState внутри использует эффект, который запускает сбор потока и записывает каждое новое значение в Compose-состояние, вызывая recomposition. Подписка автоматически прекращается, когда composable покидает экран.
Частые ошибки
- Отдавать наружу
MutableStateFlow— тогда UI сможет менять состояние в обход логики ViewModel. - Создавать ViewModel вручную через конструктор вместо
viewModel()— теряется привязка к жизненному циклу и переживание поворота. - Забыть
collectAsStateи читатьflow.valueнапрямую — UI не будет перерисовываться при изменениях.
Итог
ViewModelхранит состояние и логику и переживает пересоздание UI.StateFlowотдаёт состояние потоком; наружу — только read-only версия.collectAsStateпревращает поток в Compose-состояние и запускает перерисовку.