在界面之外使用 Compose 状态

Jetpack Compose State<T> 和 MutableState<T> 是由 Compose 快照系统支持的通用值存储器。虽然快照系统最常用于驱动界面重组,但其本身并不与界面渲染绑定。

借助 AppState 库(Transform 和 StateStore),您可以在 ViewModel、管理器、代码库和后台服务之间保留、派生、观察和持久保存 Compose State。

为何在界面之外使用 Compose State?

当您在状态观察代码块(例如界面可组合项、transform 或 derivedStateOf)中读取 Compose State 时,Compose 运行时会在架构的任何层中提供两大优势:

  • 自动和动态依赖项跟踪:读取状态的 .value(或使用 by 属性委托)会自动将该状态注册为代码块的依赖项。由于跟踪是动态的,因此依赖项是在运行时由执行的代码路径决定的。如果未采用条件分支(例如,在 if 检查或提前 return 之后),则不会读取该分支内的状态,并且对这些状态的更改不会触发不必要的重新计算。
  • 一致性和细化程度:您可以声明单独的细粒度 MutableState 属性,并将多属性写入操作封装在 Snapshot.withMutableSnapshot 中。中间更新会隔离到可变快照,并在代码块完成时以原子方式应用,因此在快照内读取的观察器永远不会看到不一致的中间状态,并且仅在它们读取的特定属性发生更改时才会重新计算。与 StateFlow 不同,您不必在单个单体式 StateFlow<UiState>(当任何字段发生更改时,都会使每个收集器失效)和单独的 StateFlow 属性(无法在单个原子事务中一起更新)之间做出选择。

在整个架构中选择合适的状态 API

根据状态应保留的时长以及需要访问该状态的组件,选择如何保留和持久保存 Compose State:

州类别 生命周期和范围 示例 推荐的 Compose State API
有效状态 始终存储在内存中;在启动时重新查询,不会在进程终止后保留。 网络连接状态、蓝牙连接状态、临时界面状态(例如内容选择)。
瞬态界面状态 在配置更改和系统发起的进程终止后继续存在,但不会无限期存储;作用域限定为单个屏幕或 activity。 展开或收起的界面元素、滚动位置、屏幕搜索查询或过滤条件切换开关。
瞬态会话状态 在后台进程终止后继续存在,但不会无限期存储;可在 activity、服务或 widget 之间共享。 正在运行的专注计时器、多步骤结账购物车或正在填写的表单草稿。
永久状态 无限期保留在磁盘上。 用户偏好设置和设置 (DataStore)、结构化应用数据和媒体库 (Room)。 使用 collectAsState 在 transform 内收集的 DataStore 或 Room flow

在 ViewModel 中使用 Compose 状态

在 ViewModel 中,您可以将独立的屏幕输入声明为精细的 MutableState 属性,使用 SavedStateHandle.saveable 在进程终止后持久保存这些属性,使用 Snapshot.withMutableSnapshot 以原子方式更新多个属性,并在 transform 中将这些属性与代码库 Flow 流组合在一起:

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 实现响应式保存状态:使用 mutableStateOf(来自 androidx.lifecycle:lifecycle-viewmodel-compose)将 searchQuery 委托给 SavedStateHandle.saveable,可在进程终止后将值保留为 MutableState,因此 transform 会自动跟踪读取。避免使用裸 SavedStateHandle.saved,它会返回一个普通的 Kotlin 属性,而不是 Compose State,并且在更改时不会触发重组。
  • 使用 Snapshot.withMutableSnapshot 进行原子多属性写入:在 Snapshot.withMutableSnapshot 内更新 searchQuery 和 selectedTaskIds 会隔离写入,并在单个原子快照事务中提交这两项更改,因此观察器(包括 transform)会在一次重组传递中同时看到这两项更新,而不是观察到新 searchQuery 与过时 selectedTaskIds 配对。
  • 使用 remember(key) 跨重组重复使用 flow:将 tasksRepository.getTasks(searchQuery) 封装在 remember 中,并使用 searchQuery 作为键,可确保当 selectedTaskIds 发生更改并重组 transform 代码块时,Compose 会重复使用现有数据库 Flow,而不是重新启动查询。

在整个应用中共享实时系统状态

对于在多个 ViewModel、activity 或服务之间共享的实时系统状态(例如网络或蓝牙连接状态),您不希望在进程终止后将过时状态保留到磁盘。而应在启动时查询当前系统状态,将其保存在应用级单例内的私有 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,无论是一次性检查、在界面可组合项内,还是在状态观察块(例如 listener、derivedStateOf 或 transform)内。例如,TaskListViewModel 可以在其 transform 块内读取 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,而无需重新查询数据库。