ViewModel и StateFlow
ViewModel хранит состояние и бизнес-логику экрана, переживает поворот, а StateFlow реактивно отдаёт это состояние в Compose через collectAsStateWithLifecycle.
Суть: ViewModel — это владелец состояния экрана, который не зависит от жизненного цикла Activity, а StateFlow обеспечивает однонаправленный поток данных из логики в интерфейс.
В уроке про жизненный цикл мы выяснили: при повороте Activity пересоздаётся и теряет данные. Решение — ViewModel. Это объект, который привязан к экрану, но переживает пересоздание Activity. В нём держат состояние и логику, а UI лишь отображает состояние и шлёт события. Это естественное продолжение паттерна state hoisting: владельцем состояния становится ViewModel.
Состояние ViewModel отдаёт через StateFlow — реактивный держатель значения, у которого всегда есть текущее состояние и который уведомляет подписчиков об изменениях. Изнутри его обновляют через приватный MutableStateFlow, а наружу отдают только для чтения.
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
data class CounterUiState(val count: Int = 0)
class CounterViewModel : ViewModel() {
private val _uiState = MutableStateFlow(CounterUiState())
val uiState: StateFlow<CounterUiState> = _uiState.asStateFlow()
fun increment() {
// обновляем состояние иммутабельно через copy
_uiState.update { it.copy(count = it.count + 1) }
}
}Подключение к Compose
В composable ViewModel получают функцией viewModel(), а его StateFlow собирают через collectAsStateWithLifecycle. Этот сборщик lifecycle-aware: он собирает данные, только пока экран на переднем плане, и приостанавливается, когда экран не виден, экономя ресурсы.
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
@Composable
fun CounterScreen(viewModel: CounterViewModel = viewModel()) {
val state by viewModel.uiState.collectAsStateWithLifecycle()
Button(onClick = { viewModel.increment() }) {
Text("Нажато: ${state.count} раз")
}
}Как работает под капотом
ViewModel создаётся через специальную фабрику и хранится в ViewModelStore, привязанном к области видимости экрана, а не к конкретному экземпляру Activity. При повороте Activity пересоздаётся, но получает ту же ViewModel — поэтому состояние сохраняется. viewModelScope — это CoroutineScope, живущий ровно столько, сколько ViewModel: когда экран окончательно уничтожается, все его корутины отменяются автоматически. collectAsStateWithLifecycle подписывается на StateFlow с учётом состояния жизненного цикла: в фоне сбор приостанавливается, а при возврате на экран возобновляется.
Поток состояния: логика --> UI
ViewModel
MutableStateFlow(count=0)
| update { copy(count+1) }
v
StateFlow (только чтение)
| collectAsStateWithLifecycle()
v
Composable рисует count
| onClick --> viewModel.increment()
^___________________________________|Частые ошибки
Хранить состояние в Activity вместо ViewModel. Тогда оно теряется при повороте — для этого ViewModel и нужна.
Выставлять наружу MutableStateFlow. Наружу отдавайте только StateFlow (через asStateFlow), чтобы UI не менял состояние напрямую.
Использовать collectAsState вместо lifecycle-версии. Обычный collectAsState не приостанавливается в фоне и может зря тратить ресурсы; предпочитайте collectAsStateWithLifecycle.
Best practices
- Состояние и логику экрана держите в ViewModel, а UI делайте тонким.
- Описывайте состояние экрана одним иммутабельным data-классом и обновляйте через
copy. - Наружу отдавайте
StateFlow, изменяйте через приватныйMutableStateFlow. - В Compose собирайте состояние через
collectAsStateWithLifecycle.
Поток «событие меняет состояние во ViewModel, UI перерисовывается» удобно смоделировать редьюсером на Python. Запустите врезку.
# Аналог ViewModel + StateFlow: состояние и редьюсер
state = {'count': 0}
def increment(s):
# иммутабельное обновление, как copy() в data-классе
return {**s, 'count': s['count'] + 1}
def render(s):
return 'Нажато: ' + str(s['count']) + ' раз'
print(render(state))
for _ in range(3):
state = increment(state) # ViewModel меняет состояние
print(render(state)) # UI перерисовываетсяПопробуй сам ▶ — добавьте редьюсер reset, возвращающий count=0. Заметьте: состояние всегда обновляется иммутабельно — новый объект, а не правка старого, ровно как через copy() в Kotlin.
Итог: ViewModel хранит состояние экрана и переживает поворот, StateFlow реактивно отдаёт его, а collectAsStateWithLifecycle безопасно собирает в Compose. Это каноничная архитектура современного экрана. Дальше подключим к ней реальные данные.