Использование Compose State за пределами пользовательского интерфейса

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, временное состояние пользовательского интерфейса, например выбор элемента.
  • mutableStateOf в управляющем аккаунте ViewModel или управляющем аккаунте @Singleton с областью действия DI
  • transform для получения состояния
  • listener для отслеживания состояния;
Временное состояние интерфейса Сохраняется при изменении конфигурации и завершении процесса системой, но не хранится бесконечно. Ограничено одним экраном или действием. Развернутые или свернутые элементы интерфейса, позиция прокрутки, поисковый запрос или переключатели фильтров.
Временное состояние сеанса Сохраняется при завершении фонового процесса, но не хранится бесконечно. Доступно для действий, сервисов и виджетов. Активный таймер фокусировки, корзина с несколькими этапами оформления заказа или черновик формы, заполняемой в данный момент.
  • StateStore, инкапсулированный в менеджер или репозиторий.
  • Ключи, реализующие PersistToDataStore и синхронизируемые с помощью syncToDataStore
Постоянное состояние Хранятся на диске бессрочно. Пользовательские настройки и параметры (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, а не State Compose, и при изменении не запускается рекомпозиция.
  • Атомарная запись нескольких свойств с помощью 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 без повторного запроса к базе данных.