State<T> и MutableState<T> в Jetpack Compose – это универсальные держатели значений, поддерживаемые системой снимков Compose. Хотя чаще всего они используются для рекомпозиции пользовательского интерфейса, сама система моментальных снимков не связана с рендерингом пользовательского интерфейса.
Библиотеки AppState – Transform и StateStore – позволяют хранить, получать, отслеживать и сохранять State Compose в ViewModel, менеджерах, репозиториях и фоновых службах.
Зачем использовать Compose State за пределами пользовательского интерфейса?
Когда вы читаете Compose State внутри блока, наблюдающего за состоянием, например UI composable, transform или derivedStateOf, среда выполнения Compose предоставляет два ключевых преимущества на любом уровне вашей архитектуры:
- Автоматическое и динамическое отслеживание зависимостей. Чтение
.valueсостояния (или использование делегирования свойствby) автоматически регистрирует это состояние как зависимость блока. Поскольку отслеживание является динамическим, зависимости определяются во время выполнения в зависимости от того, какие пути выполнения кода выполняются. Если условная ветвь не выполняется (например, после проверкиifили раннегоreturn), состояния внутри этой ветви не считываются и изменения в них не вызывают ненужных пересчетов. - Согласованность и детализация. Вы можете объявить отдельные детализированные свойства
MutableStateи обернуть записи с несколькими свойствами вSnapshot.withMutableSnapshot. Промежуточные обновления изолированы в изменяемом снимке и применяются атомарно по завершении блока, поэтому наблюдатели, считывающие данные внутри снимка, никогда не видят несогласованного промежуточного состояния и пересчитывают данные только при изменении конкретных свойств, которые они считывают. В отличие отStateFlow, вам не нужно выбирать между одним монолитнымStateFlow<UiState>(который делает недействительным каждый сборщик при изменении любого поля) и отдельными свойствамиStateFlow(которые нельзя обновить вместе в одной атомарной транзакции).
Выбор подходящего API состояния для вашей архитектуры
Выберите способ хранения и сохранения Compose State в зависимости от того, как долго должно существовать состояние и каким компонентам нужен доступ к нему:
| Категория штата | Срок действия и область применения | Примеры | Рекомендуемые API Compose State |
|---|---|---|---|
| Статус трансляции | Всегда хранится в памяти. Запрашивается заново при запуске и не сохраняется после завершения процесса. | Статус сетевого подключения, статус подключения Bluetooth, временное состояние пользовательского интерфейса, например выбор элемента. |
|
| Временное состояние интерфейса | Сохраняется при изменении конфигурации и завершении процесса системой, но не хранится бесконечно. Ограничено одним экраном или действием. | Развернутые или свернутые элементы интерфейса, позиция прокрутки, поисковый запрос или переключатели фильтров. |
|
| Временное состояние сеанса | Сохраняется при завершении фонового процесса, но не хранится бесконечно. Доступно для действий, сервисов и виджетов. | Активный таймер фокусировки, корзина с несколькими этапами оформления заказа или черновик формы, заполняемой в данный момент. |
|
| Постоянное состояние | Хранятся на диске бессрочно. | Пользовательские настройки и параметры (DataStore), структурированные данные приложения и медиатеки (Room). | Потоки DataStore или Room, собранные внутри transform с помощью collectAsState |
Использование Compose State в ViewModel
Внутри ViewModel вы можете объявить независимые входные данные экрана как детализированные свойства MutableState, сохранить их при завершении процесса с помощью SavedStateHandle.saveable, атомарно обновить несколько свойств с помощью Snapshot.withMutableSnapshot и объединить их с потоками Flow хранилища внутри transform:
sealed interface TaskListState { data object Loading : TaskListState data class Success( val tasks: List<Task>, val selectedTaskIds: Set<String>, val isOnline: Boolean = true, ) : TaskListState } @HiltViewModel class TaskListViewModel @Inject constructor( private val tasksRepository: TasksRepository, savedStateHandle: SavedStateHandle, ) : ViewModel() { var selectedTaskIds by mutableStateOf(emptySet<String>()) private set var searchQuery by savedStateHandle.saveable { mutableStateOf("") } private set fun updateSearchQuery(query: String) { Snapshot.withMutableSnapshot { searchQuery = query selectedTaskIds = emptySet() } } val tasksState: State<TaskListState> = transform<TaskListState>( initialValue = TaskListState.Loading, scope = viewModelScope, ) { val tasks = remember(searchQuery) { tasksRepository.getTasks(searchQuery) }.collectAsState(initial = null).value ?: return@transform TaskListState.Loading TaskListState.Success( tasks = tasks, selectedTaskIds = selectedTaskIds, ) } }
Ключевые закономерности в этом ViewModel:
- Используйте
SavedStateHandle.saveableдля реактивного сохраненного состояния. ДелегированиеsearchQueryвSavedStateHandle.saveableс помощьюmutableStateOf(изandroidx.lifecycle:lifecycle-viewmodel-compose) сохраняет значение при завершении процесса какMutableState, поэтомуtransformавтоматически отслеживает чтения. Не используйтеSavedStateHandle.savedбез дополнительных элементов, так как в этом случае возвращается обычное свойство Kotlin, а неStateCompose, и при изменении не запускается рекомпозиция. - Атомарная запись нескольких свойств с помощью
Snapshot.withMutableSnapshot. ОбновлениеsearchQueryиselectedTaskIdsвнутриSnapshot.withMutableSnapshotизолирует запись и фиксирует оба изменения в одной атомарной транзакции снимка. В результате наблюдатели (включаяtransform) видят оба обновления вместе в одном проходе рекомпозиции, а не новое значениеsearchQueryв паре с устаревшим значениемselectedTaskIds. - Повторное использование потоков в рекомпозициях с помощью
remember(key). ОбертываниеtasksRepository.getTasks(searchQuery)вrememberс ключомsearchQueryгарантирует, что когдаselectedTaskIdsизменится и рекомпонует блокtransform, Compose повторно использует существующий поток базы данныхFlow, а не перезапустит запрос.
Передавайте данные о состоянии системы в реальном времени в приложение.
Для текущего состояния системы, которое используется в нескольких ViewModel, действиях или сервисах, например статуса подключения к сети или Bluetooth, не нужно сохранять устаревшие данные на диск при завершении процесса. Вместо этого запрашивайте текущее состояние системы при запуске, сохраняйте его в закрытом свойстве mutableStateOf внутри синглтона на уровне приложения и предоставляйте доступ только для чтения State<T>:
interface NetworkMonitor { val isOnline: State<Boolean> } @Singleton class LiveNetworkMonitor @Inject constructor( @ApplicationContext context: Context, ) : NetworkMonitor { private val connectivityManager = context.getSystemService(ConnectivityManager::class.java) private val _isOnline = mutableStateOf(checkCurrentConnectivity()) override val isOnline: State<Boolean> = _isOnline private val callback = object : ConnectivityManager.NetworkCallback() { override fun onCapabilitiesChanged( network: Network, networkCapabilities: NetworkCapabilities, ) { _isOnline.value = networkCapabilities.hasCapability( NetworkCapabilities.NET_CAPABILITY_INTERNET ) } override fun onLost(network: Network) { _isOnline.value = false } } init { connectivityManager.registerDefaultNetworkCallback(callback) } private fun checkCurrentConnectivity(): Boolean { val network = connectivityManager.activeNetwork ?: return false val capabilities = connectivityManager.getNetworkCapabilities(network) ?: return false return capabilities.hasCapability( NetworkCapabilities.NET_CAPABILITY_INTERNET ) } }
MutableState поддерживает согласованное чтение и обновление из любого потока, поэтому NetworkCallback может обновлять _isOnline.value непосредственно из своего фонового потока обратного вызова. Если фоновый обратный вызов обновляет несколько свойств MutableState, оберните операции записи в Snapshot.withMutableSnapshot, чтобы они применялись атомарно.
Любой компонент в приложении может считывать networkMonitor.isOnline.value – будь то однократная проверка, внутри composable-функции пользовательского интерфейса или внутри блока наблюдения за состоянием, такого как listener, derivedStateOf или transform. Например, в блоке transform файла TaskListViewModel может быть указано следующее:
networkMonitor.isOnline.value
val tasksState: State<TaskListState> = transform<TaskListState>( initialValue = TaskListState.Loading, scope = viewModelScope, ) { val tasks = remember(searchQuery) { tasksRepository.getTasks(searchQuery) }.collectAsState(initial = null).value ?: return@transform TaskListState.Loading TaskListState.Success( tasks = tasks, selectedTaskIds = selectedTaskIds, isOnline = networkMonitor.isOnline.value, ) }
Поскольку transform отслеживает networkMonitor.isOnline.value вместе с searchQuery, selectedTaskIds и репозиторием Flow, любое изменение в подключении автоматически пересчитывает TaskListState.Success без повторного запроса к базе данных.