应用基础知识

借助 Jetpack AppState,让 Compose 状态超越界面

扬声器

  • Ben Sagmoe

    开发者关系工程师

  • Elif Bilgin Morris

    软件工程师

关于

演讲简介

Compose 的快照运行时、自动依赖项跟踪和原子更新不一定非要停留在界面层。借助 Jetpack AppState,您可以将 Compose 状态上移,在状态到达界面之前,使用快照系统提升和派生状态,从而简化到达屏幕的内容。

在本课程中,您将了解 AppState 的两个基础库。我们将深入探讨用于提升状态的 StateStore,以及用于通过组合和普通 Kotlin 控制流派生状态的 Transform,从而消除复杂的 Flow 组合链,让界面轻松保持同步。

摘要

  • Compose Runtime 可作为通用状态管理引擎,无需 Compose UI。其快照系统会动态跟踪读取操作,并确保各属性的并发更新保持一致。
  • Jetpack AppState Transform 会通过无头可组合块计算并公开 Compose 状态。它取代了复杂的 Flow 运算符链(如 combine),同时支持提前返回和嵌套状态派生。
  • Transform 使用 Eager 无头帧时钟在界面线程之外运行转换。只要监控的状态保持不变,时钟就会暂停,从而避免不必要的工作。
  • Jetpack AppState StateStore 可在 activity 和后台服务等不同组件之间共享 Compose 状态。将其与 DataStore 集成后,无需自定义样板代码,即可让各个键在进程终止后继续存在。

资源

Gemini Notebook

主题

  • jetpack-compose
  • jetpack-appstate
  • state-management
  • android-architecture
  • compose-runtime

转写文稿

简介

  • 大家好,欢迎观看“使用 Jetpack AppState 将 Compose 状态扩展到界面之外”视频。

  • 我叫 Ben,是一名开发者关系工程师,负责 Jetpack 架构库。

  • 我是 Elif,是负责应用架构库的工程师之一。

  • 今天,我们要和大家聊聊 Compose 的未来:它将超越界面,深入到应用的数据层和领域层。

  • 相信大家现在已经非常清楚,Compose 是 Android 界面开发推荐使用的工具包。

  • 从手机和平板电脑到微件、手表、电视和 XR 设备,Compose 都能提供强力支持。

  • 但随着应用在这些界面和后台组件中不断扩展,管理其背后的状态变得越来越困难,尤其是在两个常见痛点方面。

  • 第一种是派生复杂状态。

  • 将多个数据流、本地界面输入和环境状态(例如网络连接)交织在一起,通常会变成一团乱麻,其中包含 combine、flatMapLatest 和 distinctUntilChanged 等运算符。

  • 第二种情况是跨组件共享状态:让 activity、服务、应用 widget 等保持同步,并确保该状态在需要时能够经受住进程终止的考验。

  • 当我们退一步审视这些挑战时,我们开始研究 Compose 本身的构建方式。

  • 大多数人认为 Compose 是一个界面工具包,但实际上它由两个不同的层组成:顶层的 Compose UI 和底层的 Compose Runtime。

  • Compose UI 负责处理布局、绘制和输入,而 Compose 运行时是一个用于状态管理和重组的通用引擎。

  • 它不依赖于界面。

  • 正是这一认识促使我们构建了 Jetpack AppState。

  • 它是一个库系列,专注于在界面之外充分利用 Compose 状态。

什么是 Compose State?

  • 但在深入探讨 AppState 之前,我们先来明确一下 Compose 状态的含义。

  • 答案是 androidx.compose.runtime.State。

    /**
     * A value holder where reads to the [value] property during the execution of a [Composable]
     * function, the current [RecomposeScope] will be subscribed to changes of that value.
     */
    @Stable
    public interface State<out T> {
        public val value: T
    }
  • 以及您可能更熟悉的 MutableState 接口。

    /**
     * A mutable value holder where reads to the [value] property during the execution of a [Composable]
     * function, the current [RecomposeScope] will be subscribed to changes of that value. When the
     * [value] property is written to and changed, a recomposition of any subscribed [RecomposeScope]s
     * will be scheduled. If [value] is written to with the same value, no recompositions will be
     * scheduled.
     */
    @Stable
    public interface MutableState<T> : State<T> {
        override var value: T
        public operator fun component1(): T
        public operator fun component2(): (T) -> Unit
    }
  • 从根本上说,这些只是最基本的容器。

  • Compose 快照系统才是让它们真正发挥作用的关键。

  • 因此,当我们说 Compose 状态时,通常指的是快照状态。

  • 因此,当您调用 mutableStateOf 时,您是在创建一个容器,该容器会通过快照系统路由读取和写入操作。

  • 每当您的代码在快照观察器(例如 Composition)中运行时,您都会获得 Flow 等数据流无法提供的两个属性。

    @Composable
    fun Counter() {
        val count = remember { mutableStateOf(0) }
    }
  • 第一项是自动和动态读取跟踪。

    @Composable
    fun Counter() {
        val count = remember { mutableStateOf(0) }
        Column {
            Text("Count: ${count.value}")
            AddButton(onClick = { count.value++ })
        }
    }
  • 因此,“自动”意味着无需订阅或收集。

  • 仅读取 .value 即为订阅。

  • 因此,如果我们在这里查看 Counter,直接在可组合项中读取 count.value 会向组合报告该读取操作,并且组合会在其读取器列表中注册 Counter。

  • 现在,我们来看看 AddButton。

    @Composable
    fun Counter() {
        val count = remember { mutableStateOf(0) }
        Column {
            Text("Count: ${count.value}")
            AddButton(onClick = { count.value++ })
        }
    }
    
    @Composable
    fun AddButton(onClick: () -> Unit) {
        Button(onClick = onClick) {
            Text("Add")
        }
    }
  • 它仅在点击时写入计数。

  • 因此,它不是读取器。

  • 当用户点击 AddButton 并修改计数时,快照系统会跟踪该写入操作,从而让组合知道要使 Counter 失效,而不是 AddButton。

  • 动态意味着读取跟踪会在运行时根据实际运行的代码路径进行。

    @Composable
    fun Counter() {
        val isVisible = remember { mutableStateOf(true) }
        val count = remember { mutableStateOf(0) }
        Column {
            if (isVisible.value) {
                Text("Count: ${count.value}")
            } else {
                Text("Hidden")
            }
            AddButton(onClick = { count.value++ })
        }
    }
  • 因此,在这里,我们使用 isVisible 标志来保护计数器文本。

  • 当 isVisible 为 true 时,系统会同时读取 isVisible 和计数。

  • 因此,Counter 会注册为两者的读取器。

  • 当 isVisible 为 false 时,如果 if 分支不运行,则不会读取 count,因此 Counter 只会注册为 isVisible 的读取器,而不是 count 的读取器。

    @Composable
    fun Counter() {
        val isVisible = remember { mutableStateOf(false) }
        val count = remember { mutableStateOf(0) }
        Column {
            if (isVisible.value) {
                Text("Count: ${count.value}")
            } else {
                Text("Hidden")
            }
            AddButton(onClick = { count.value++ })
        }
    }
  • 因此,当 AddButton 写入计数时,没有任何内容需要失效。

  • 现在,第二个属性是无需在一致性和精细度之间做出选择。

  • 一致性意味着,当您更新快照中的多个值时,这些更改在代码运行期间会保持隔离,即使另一个线程同时在写入这些值也是如此。

  • 快照完成后,这些更新会同时显示。

  • 粒度意味着可以单独声明和跟踪不同的属性,尤其是在它们不相关时。

  • 因此,更新一个属性只会使实际依赖于该属性的读取器失效。

  • 我们来看一下实际代码中的示例。

  • 例如,我们来看一款具有三种状态的游戏:关卡名称、存活时间和得分。

    val levelName: String = "Level 1 - Stay Alive!"
    val timeSurvived: Int = 0
    val score: Double = 0.0
  • 首先,我们来看一个 Flow 实现,它通过将所有内容捆绑到一个 GameUiState 类中,以牺牲粒度为代价来选择一致性。

    data class GameUiState(
        val levelName: String = "Level 1 - Stay Alive!",
        val timeSurvived: Int = 0,
        val score: Double = 0.0
    )
  • 这意味着,每次 updateScore 运行时,您都会复制整个 GameUiState,以便 timeSurvived 和得分能够同步更新。

    data class GameUiState(
        val levelName: String = "Level 1 - Stay Alive!",
        val timeSurvived: Int = 0,
        val score: Double = 0.0
    )
    
    class GameViewModel : ViewModel() {
        val uiState = MutableStateFlow(GameUiState())
        fun updateScore() = uiState.update {
            it.copy(
                timeSurvived = it.timeSurvived + 1,
                score = it.score + 10 + .01 * it.timeSurvived
            )
        }
    }
  • 在界面中,GameScreen 会收集界面状态并从中读取数据,以便将相关属性向下传递给 Header 和 ScoreBadge。

    data class GameUiState(
        val levelName: String = "Level 1 - Stay Alive!",
        val timeSurvived: Int = 0,
        val score: Double = 0.0
    )
    
    class GameViewModel : ViewModel() {
        val uiState = MutableStateFlow(GameUiState())
        fun updateScore() = uiState.update {
            it.copy(
                timeSurvived = it.timeSurvived + 1,
                score = it.score + 10 + .01 * it.timeSurvived
            )
        }
    }
    
    @Composable
    fun GameScreen(viewModel: GameViewModel = viewModel()) {
        val state by viewModel.uiState.collectAsStateWithLifecycle()
        Column {
            Header(levelName = state.levelName)
            ScoreBadge(
                timeSurvived = state.timeSurvived,
                score = state.score
            )
        }
    }
  • 由于 GameScreen 会读取每个状态,因此每次调用 updateScore 都会使 GameScreen 失效,即使标题的数据从未发生变化。

  • 现在,您可以将每个属性拆分为各自的 Flow 以实现精细度,但这样会失去一致性,因为另一个线程可能会在另一个更新块运行时更新值。

  • 现在,我们来考虑另一种基于状态的实现。

    @Stable
    class GameUiState {
        var levelName by mutableStateOf("Level 1 - Stay Alive!")
        var timeSurvived by mutableStateOf(0)
        var score by mutableStateOf(0.0)
    }
  • 在此示例中,我们再次使用 GameUiState。

  • 但在此示例中,情况略有不同,因为每个属性都委托给 mutableStateOf。

  • 在这里,updateScore 将其写入操作封装在 withMutableSnapshot 中,这使我们能够在写入操作之间保持一致性。

    @Stable
    class GameUiState {
        var levelName by mutableStateOf("Level 1 - Stay Alive!")
        var timeSurvived by mutableStateOf(0)
        var score by mutableStateOf(0.0)
    }
    
    class GameViewModel : ViewModel() {
        val uiState = GameUiState()
        fun updateScore() = Snapshot.withMutableSnapshot {
            uiState.timeSurvived += 1
            uiState.score += 10 + .01 * uiState.timeSurvived
        }
    }
  • 然后,在界面中,GameScreen 将状态直接传递给 Header 和 ScoreBadge。

    @Stable
    class GameUiState {
        var levelName by mutableStateOf("Level 1 - Stay Alive!")
        var timeSurvived by mutableStateOf(0)
        var score by mutableStateOf(0.0)
    }
    
    class GameViewModel : ViewModel() {
        val uiState = GameUiState()
        fun updateScore() = Snapshot.withMutableSnapshot {
            uiState.timeSurvived += 1
            uiState.score += 10 + .01 * uiState.timeSurvived
        }
    }
    
    @Composable
    fun GameScreen(viewModel: GameViewModel = viewModel()) {
        val state = viewModel.uiState
        Column {
            Header(state = state)
            ScoreBadge(state = state)
        }
    }
  • 它不会读取任何属性,因此当 updateScore 运行时,GameScreen 不是读取器。

  • 只有实际组件(在本例中为 ScoreBadge)会失效。

  • 最后,请注意,这些内容都与界面无关。

  • 界面恰好是这些属性真正发挥作用的地方。

  • 以上就是本次演讲的全部内容,感谢大家的聆听。

  • 好了,不开玩笑了,接下来有请 Elif 为我们介绍第一个库:Transform。

Transform 简介

  • 好的。

  • 谢谢 Ben。

  • 正如 Ben 刚才提到的,Compose 快照状态为我们提供了自动和动态读取跟踪、一致性和精细度,这让我想到一个问题:如果我们的数据流也可以由 Compose 状态快照(即快照状态)驱动,会怎么样?

  • 如果状态是我们的数据模型,那么我们该如何为所有使用方实际计算和转换状态呢?

  • 这时,全新的 Jetpack AppState Transform 库就派上用场了。

  • 在深入了解该库之前,我们需要先了解该库的由来。

  • 因此,利用 Compose 引擎(即此处的运行时和编译器)在 Headless 组合中生成状态值的想法并不新鲜。

  • 许多人可能已经在使用或熟悉 Molecule 库。

  • 这里的“无头”是指没有界面。

  • 因此,Molecule 率先展示了使用 Compose Runtime 的强大功能,它通过实际行动向我们展示了可组合函数不一定非要发出界面节点,实际上还可以用来编写返回纯数据模型的可组合函数。

  • 在 Molecule 中,这是一个 Kotlin Flow 或 StateFlow。

  • 这意味着,您无需链接 combine 等响应式运算符,而是可以编写普通的 Kotlin 代码。

  • 不过,既然界面已经采用 Compose,那么如果数据模型是 State,而不是 Flow,会怎么样?

  • 使用 Transform 时,数据模型就是状态。

  • 我们来看看实际的 API。

    public fun <R> transform(
        initialValue: R,
        scope: CoroutineScope,
        onUpdate: @Composable (R) -> R,
    ): State<R>
  • 这就是整个 API。

  • 它非常简单,只有一个函数。

  • 它会返回一个普通的可观察 State,而不是 Flow,也不是 StateFlow。

  • 而使用该 PCollection 的任何人都不知道也不关心它是由转换生成的。

  • 这里真正有趣的是这个代码块。

  • 可组合。

  • 这意味着,您已经知道的所有内容都可以在其中运行。

  • 请记住,LaunchedEffect、collectAsState、提前返回、普通 if 语句,以及您可以在可组合项中读取的任何内容,都可以在转换中派生。

  • 需要注意的是,今天在演示中看到的所有 API 都处于 Alpha 版阶段,可能会发生变化,但无论如何,我们还是来看一个示例。

  • 我们来看一个非常简单的小型专注计时器应用。

  • 你选定一项任务,设置一个 25 分钟的计时器,然后开始倒计时。

  • 每次创建单个界面时,即使是像这样简单的单个界面,您也可能需要合并和管理多个独立的数据流,以便在后台组合状态。

  • 对于专注屏幕,假设我们有两个:专注时段本身,它将显示我们处于哪个阶段,即是否暂停、还剩多少秒,所有这些都将来自一个计时器引擎,该引擎将作为一个 StateFlow,假设每秒计时一次。

  • 另一个数据流将是用户当前正在处理的任务,该任务将以 Flow 的形式从 Room 数据库传入。

  • 因此,最后会有两个数据流和一个屏幕状态。

  • ViewModel 是将它们组合在一起的地方,其输出(我们称之为摘要)将是我们要编写的下一段代码。

  • 传统上,您需要在 ViewModel 中使用链式运算符来组合这些 flow。

    class FocusScreenViewModel(
        focusManager: FocusSessionManager,
        taskRepository: TaskRepository
    ) : ViewModel() {
        val summary: StateFlow<FocusSummary> =
            combine(focusManager.sessionFlow, taskRepository.tasksFlow) { session, tasks ->
                if (session.phase == FocusPhase.Idle) {
                    FocusSummary.Idle
                } else {
                    val task = tasks.find { it.id == session.activeTaskId }
                    FocusSummary.Active(...)
                }
            }.stateIn(
                    scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5_000),
            initialValue = FocusSummary.Idle,
    )
    }
  • 因此,将这两个流合并为一个流,在 lambda 内部进行分支,即使用 if-else 语句,最后再添加一个状态。

  • 借助 Transform,我们可以省去所有这些样板代码。

    class FocusScreenViewModel(
        focusManager: FocusSessionManager,
        taskRepository: TaskRepository
    ) : ViewModel() {
        val summary: State<FocusSummary> =
            transform<FocusSummary>(
    initialValue = FocusSummary.Idle,
    scope = viewModelScope,
    container = handle.getOrCreateContainer("transform")
    ) {
                val session by focusManager.sessionFlow.collectAsState()
                if (session.phase == FocusPhase.Idle) return@transform FocusSummary.Idle
                val tasks by taskRepository.tasksFlow.collectAsState(emptyList())
                val task = tasks.find { it.id == session.activeTaskId }
                FocusSummary.Active(...)
  • 此调用看起来有点眼熟,因为我们传入了作用域中的初始值,但上游没有任何内容可链接,派生就是代码块本身。

  • 与 stateIn 不同,在 中,您可能需要配置 WhileSubscribed 超时来暂停和恢复上游数据流,而转换会在该作用域的生命周期内持续运行。

  • 同样,我们的来源是 flow,并且欢迎使用 flow。

    class FocusScreenViewModel(
        focusManager: FocusSessionManager,
        taskRepository: TaskRepository
    ) : ViewModel() {
        val summary: State<FocusSummary> =
            transform<FocusSummary>(
                initialValue = FocusSummary.Idle,
                scope = viewModelScope,
                       container = handle.getOrCreateContainer("transform")
            ) {
                val session by focusManager.sessionFlow.collectAsState()
                if (session.phase == FocusPhase.Idle)
                    return@transform FocusSummary.Idle
                val tasks by taskRepository.tasksFlow.collectAsState(emptyList())
                val task = tasks.find {
                    it.id == session.activeTaskId
                }
                         FocusSummary.Active(...)
            }
    }
  • 我们将使用 collectAsState 桥接每个流,由于会话是 StateFlow,因此我们不需要初始值,但 Room Flow 是冷流,所以我们为其提供了一个初始值。

  • 以上就是订阅的全部内容。

  • 读取操作会订阅该数据。

  • 这就是 Ben 在我们之前看到的示例中提到的自动读取跟踪,但它是在 ViewModel 中。

  • 我们甚至可以在处于空闲状态时,像这样引入提前返回。

  • 假设我们没有新任务,甚至不会处理其余任务。

  • 同样,这也是动态读取跟踪发挥作用的结果。

  • 当应用处于空闲状态时,Room Flow 根本不会被收集。

  • 最后,我们输出摘要。

  • 因此,如果用户暂停任务或计时器计时,所有内容都会自动更新。

嵌套转换

  • 因此,当我们跳出单个 ViewModel 示例时,情况会变得稍微复杂一些。

  • 假设同一会话还需要显示在另外两个平台上:一个位于应用每个屏幕底部的迷你计时器栏,以及一个主屏幕微件。

  • 这两者都需要有关会话摘要的相同派生信息,即哪个阶段、是否暂停、计时器还剩多少时间。

  • 这意味着,如果每个平台都要自行组装一个副本,那么同一个派生过程就要在三个不同的地方执行三次,而且这三个地方必须保持一致。

  • 因此,我们需要在应用范围内派生一次摘要,然后下游的所有内容都将使用该摘要。

  • 那么,我们该如何实际实施这样的方案呢?

  • 我们的实现可能需要三个部分。

    class FocusSessionManager(taskRepository: TaskRepository, scope: CoroutineScope) {
        ...
    }
    
    class MiniTimerViewModel(focusManager: FocusSessionManager) : ViewModel() {
        ...
    }
    
    class FocusWidget : GlanceAppWidget() {
        override suspend fun provideGlance(context: Context, id: GlanceId) {
            provideContent {
                ...
            }
        }
    }
  • 因此,应用范围内的会话管理器将用于生成摘要,而迷你计时器 ViewModel 和 Glance 微件都将使用此专注会话管理器生成的摘要。

  • 我们先用传统方式构建每个模型,然后再用 Transform 构建。

  • 因此,按照传统做法,我们首先会在应用范围内将摘要派生一次,作为 StateFlow。

    class FocusSessionManager(
        taskRepository: TaskRepository,
        scope: CoroutineScope
    ) {
        val sessionFlow: StateFlow<FocusSession> = ...
        val summary: StateFlow<FocusSummary> =
            combine(
                sessionFlow,
                taskRepository.tasksFlow
            ) { session, tasks ->
                if (session.phase == FocusPhase.Idle) {
                    FocusSummary.Idle
                } else {
                    val task = tasks.find {
                        it.id == session.activeTaskId
                    }
                    FocusSummary.Active(...)
                }
            }.stateIn(
    scope = appScope,
    started = SharingStarted.WhileSubscribed(5_000),
    initialValue = FocusSummary.Idle
    )
    }
  • 这与我们之前在 ViewModel 示例中看到的 combine 和 stateIn 调用相同,只是上移了一个级别。

  • 借助 Transform,我们还可以重复使用前面看到的 ViewModel 中的相同声明。

    class FocusSessionManager(
        taskRepository: TaskRepository,
        scope: CoroutineScope
    ) {
        val sessionFlow: StateFlow<FocusSession> = ...
        val summary: State<FocusSummary> =
            transform<FocusSummary>(
               initialValue = FocusSummary.Idle,
    scope = scope,
    container = handle.getOrCreateContainer("transform")
            ) {
                val session by sessionFlow.collectAsState()
                if (session.phase == FocusPhase.Idle)
                    return@transform FocusSummary.Idle
                val tasks by taskRepository.tasksFlow
                    .collectAsState(emptyList())
                val task = tasks.find {
                    it.id == session.activeTaskId
                }
                FocusSummary.Active(...)
            }
    }
  • 唯一的变化是,它现在位于应用范围内。

  • 接下来是迷你计时器的 ViewModel。

    class MiniTimerViewModel(
        focusManager: FocusSessionManager
    ) : ViewModel() {
        val isExpanded = MutableStateFlow(false)
    
        val uiState: StateFlow<MiniTimerUiState> =
            combine(
                focusManager.summary,
                isExpanded
            ) { summary, expanded ->
                MiniTimerUiState(
        summary = summary,
        showControls = expanded && summary is FocusSummary.Active
    )
            }.stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5_000),
            initialValue = MiniTimerUiState()
    )
    
        fun toggleExpanded() { isExpanded.update { !it } }
    }
  • 假设我们想在这里也给自己增加一点挑战。

  • 假设在迷你计时器中,除了已有的两个数据流(StateFlow 和 Room Flow)之外,我们还想跟踪计时器栏是否已展开。

  • 为此,我们使用 MutableStateFlow。

  • 为此,我们将不得不使用另一个 combine 来跟踪摘要流和 isExpanded StateFlow。

  • 我们再添加一个状态。

  • 借助 Transform,我们无需再次使用这些链式运算符即可实现数据流之间的同步。

    class MiniTimerViewModel(
        focusManager: FocusSessionManager
    ) : ViewModel() {
        val isExpanded = MutableStateFlow(false)
    
        val uiState: State<MiniTimerUiState> =
            transform(
                initialValue = MiniTimerUiState(),
                scope = viewModelScope,
                       container = handle.getOrCreateContainer("transform")
            ) {
                val summary by focusManager.summary
                       val expanded by isExpanded.collectAsState()
                MiniTimerUiState(
        summary = summary,
        showControls = expanded && summary is FocusSummary.Active
    )
            }
    
        fun toggleExpanded() { isExpanded.update { !it } }
    }
  • 我们可以做的是,这真的很酷,我们可以做嵌套转换。

  • 因此,在这里,ViewModel 会读取摘要(即上游转换的输出),就像读取任何其他状态一样,然后使用 collectAsState 收集 isExpanded。

  • 因此,我们可以在 StateFlow 的同一代码块中混合使用上游状态和下游数据流。

  • 同时牢记粒度

  • 因此,如果您切换 isExpanded,系统只会重新运行此代码块。

  • 应用级块和 widget 甚至无需注意。

  • 最后是微件。

    class FocusWidget : GlanceAppWidget() {
        override suspend fun provideGlance(
            context: Context,
            id: GlanceId
        ) {
            val focusManager = ...
            provideContent {
                val summary by focusManager.summary
                    .collectAsState()
                GlanceTheme { FocusWidgetContent(summary) }
            }
        }
    }
  • 由于 Glance 是 Compose,因此我们可以像任何其他可组合项一样,在 provideContent 中使用 collectAsState 收集 StateFlow。

  • 借助 Transform,这一过程变得更加简单。

    class FocusWidget : GlanceAppWidget() {
        override suspend fun provideGlance(
            context: Context,
            id: GlanceId
        ) {
            val focusManager = ...
            provideContent {
                val summary by focusManager.summary
                GlanceTheme { FocusWidgetContent(summary) }
            }
        }
    }
  • 焦点管理器会为我们提供 Compose 状态,因此我们无需在此处进行任何转换。

  • 最终,迷你计时器栏和主屏幕 widget 绝不会出现不一致的情况,因为它们读取的是相同的状态。

  • 不仅如此,对该状态所做的任何更改都会反映到该状态的所有使用方。

  • 这是什么意思?

  • 因此,如果您在此处的小组件上点击暂停,该操作将通过管理器进入会话流,再通过摘要传出,这三个屏幕都会同步更新。

  • 现在,我们已经了解了如何使用 Transform,接下来让我们深入探究其底层机制。

  • 我们之前看到的 Transform API 实际上是一个辅助函数,用于创建基本的托管式 Transform。

    public fun <R> transform(
        initialValue: R,
        scope: CoroutineScope,
        onUpdate: @Composable (R) -> R,
    ): State<R> {
            val transformScope = TransformScope()
        val transform = transformScope
    .launchTransform(initialValue, onUpdate)
        scope.launch(
    dispatcher,
    start = CoroutineStart.UNDISPATCHED
     ) {
            try { transformScope.run() } finally {
                transform.dispose()
            }
        }
        return transform
    }
  • 实际上,它使用的是 TransformScope 和 launchTransform 扩展函数,该函数会在此 TransformScope 中启动可组合转换,并返回一个可释放状态。

  • 从上到下阅读该文件,您会发现它会为您完成所有操作。

  • 它会创建 TransformScope 并将您的转换启动到其中,这将是您返回的状态。

  • 然后,它还会在您提供的调度程序上启动 TransformScope 中的运行,以便作用域的生命周期和线程属于您。

  • 最后,在作用域结束时,它会处置您的转换。

  • 这就是 Transform 的整个生命周期。

  • 我们来快速了解一下 TransformScope 界面。

    public fun TransformScope(context: CoroutineContext = Dispatchers.Default): TransformScope
    
    public abstract class TransformScope {
            public fun <T> createTransform(initialState: T, onUpdate: @Composable (T) -> T): DisposableState<T>
    public abstract suspend fun run()
    }
    
    public fun <T> TransformScope.launchTransform(
    initialState: T,
    onUpdate: @Composable (T) -> T,
    ): DisposableState<T>
    
    public interface DisposableState<T> : State<T> {
    public fun dispose()
    }
  • 我们可以看到,它有一个 createTransform 函数,该函数将创建一个附加到此 TransformScope 的 Transform。

  • 它还具有一个挂起运行函数,该函数会启动 TransformScope 并开始更新当前状态转换。

  • 这是什么意思?

  • 因此,run 实际上会设置 Recomposer 和组合。

  • 该作用域内的每个 Transform 都位于该组合内,并且有自己的重组作用域。

  • 这就是我们想要共享作用域的原因,也是我们拥有 TransformScope 的原因。

  • 这样,每当状态发生变化时,只有读取该状态的转换才会重新运行。

  • 如前所述,run 是一个挂起函数。

    override suspend fun run() {
            val context = currentCoroutineContext()
            val clock = context[MonotonicFrameClock] ?: HeadlessTransformClock()
            ...
            val recomposer = Recomposer(context)
            val composition = Composition(UnitApplier, recomposer)
            try {
                withContext(clock) {
                    if (clock is HeadlessTransformClock) {
                        launch(start = CoroutineStart.UNDISPATCHED) {
                            clock.runClock()
                        }
                    }
                    ...
                }
            } finally {
                // dispose of composition, cancel the clock, close the recomposer
            }
        }
  • 因此,它会在您调用它的任何协程上运行,直到该协程被取消。

  • 现在,您可能会问或想知道,Recomposer 需要一个帧时钟来告诉它何时需要生成帧。

  • 在 Compose 界面中,这就是显示效果,但正如我们之前提到的,我们处于无头环境中,因此没有界面。

  • 因此,除非您向我们提供时钟,否则我们将自行创建。

  • 通过将重组与显示帧时钟解耦,Transform 会在界面线程之外运行其所有转换和状态派生。

  • 为此,我们构建了专用的 HeadlessTransformClock。

  • 转换帧时钟。

  • 由于微件、服务或其他下游 Transform 可能会等待 Transform 的值,因此我们的时钟会急切地进行计算。

  • 这意味着,只要 Transform 使用的任何状态发生变化,系统就会立即生成新帧。

  • 它是一个小型无锁状态机,因此请求帧是一个简单的比较并设置操作。

  • 当状态没有发生任何变化时,时钟会停止。

  • 因此,如果转换使用的状态完全没有发生变化,则不会产生任何费用。

  • 在将时间交还给 Ben 之前,我们还应注意,Transform 现在支持 rememberSaveable,以确保您的数据在配置更改后仍能保存。

  • 它通过新的 SavedStateContainer API 实现此目的,该 API 现在是 Compose 的 SaveableStateRegistry、SavedStateHandle 和 SavedStateRegistry 的构建基础。

  • 您可以将其中任何一个转换为 SavedStateContainer,也可以创建子容器并将其传递到 Transform 中,以确保数据得到保存。

  • 最后,再次提醒大家,我们今天讨论的所有 API 都处于 Alpha 版阶段,因此可能会发生变化。

  • 接下来,我将把时间交还给 Ben。

界面之外的 Compose 状态

  • 好了

  • 好的,谢谢 Elif。

  • 现在,我们已经了解了 Compose 状态的强大功能,以及 Transform 如何让我们在界面之外派生状态。您可能已经注意到,到目前为止,示例中的输入都是我们使用 collectAsState 桥接的 Flow。

  • 但这样做总是必要的吗?

  • 为什么不直接在状态容器中使用 Compose 状态?

  • 为了回答这个问题,我发现将状态分为三大类很有帮助。

  • 第一种是仅存储在内存中的实时状态。

  • 例如网络或蓝牙连接状态。

  • 您不希望使用持久化值。

  • 第二种是应在进程终止后继续保留但无需无限期存储的临时状态。

  • 例如界面状态(如卡片是否展开)和会话状态(如导航返回堆栈或表单草稿)。

  • 第三种是永久状态,会无限期存储。

  • 例如,数据库中的用户偏好设置或媒体库。

  • 首先,我们来看看如何将 Compose 状态用于实时状态和瞬态。

  • 到目前为止,我们的架构指南将 Compose 状态保留在界面层中,有时也保留在 ViewModel 中,并建议您在其他所有位置使用 Flow。

  • 但为什么会这样呢?

  • 这是因为,一旦需要将状态属性与 Flow 结合使用,就必须使用 snapshotFlow 将其转换回来。

  • 但有了 Transform,就不需要进行这种转换了。

  • 我们来看看这在 ViewModel 中意味着什么。

    @HiltViewModel
    class TaskListViewModel @Inject constructor(
        private val tasksRepository: TasksRepository,
        private val networkMonitor: NetworkMonitor,
        savedStateHandle: SavedStateHandle,
    ) : ViewModel() {
        ...
    }
  • 对于本地屏幕输入(例如此处的 selectedTaskIds),您可以声明普通 mutableStateOf 属性,而不是 MutableStateFlow。

    @HiltViewModel
    class TaskListViewModel @Inject constructor(...) : ViewModel() {
        var selectedTaskIds by mutableStateOf(emptySet<String>())
            private set
    }
  • 由于您不一定希望在进程终止后保留这些数据,因此仅使用 mutableStateOf 也是可以的。

  • 对于您希望在进程终止后保留的内容(例如搜索查询),您可以使用 savedStateHandle.saveable 直接支持其可变状态。

    @HiltViewModel
    class TaskListViewModel @Inject constructor(...) : ViewModel() {
        var selectedTaskIds by mutableStateOf(emptySet<String>())
            private set
    
        var searchQuery by savedStateHandle.saveable { mutableStateOf("") }
            private set
    }
  • 然后,就像前面游戏示例中的 updateScore 一样,当某个方法一次更新多个状态值时,将写入操作封装在 withMutableSnapshot 中可确保一致性。

    @HiltViewModel
    class TaskListViewModel @Inject constructor(...) : 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()
            }
        }
    }
  • 然后,如需将 searchQuery 与 Room Flow 结合使用,您只需在收集 Flow 时直接在 Transform 中读取它。

    val tasksState: State<TaskListState> = transform(
        initialValue = TaskListState.Loading,
        scope = viewModelScope,
    ) {
        val tasks = remember(searchQuery) {
            tasksRepository.getTasks(searchQuery)
        }.collectAsState(initial = null).value
            ?: return@transform TaskListState.Loading
    
        TaskListState.Success(tasks = tasks)
    }
  • 那么,网络连接等实时状态呢?

    @Singleton
    class NetworkMonitor @Inject constructor(
        @ApplicationContext context: Context,
    ) {
        private val connectivityManager =
            context.getSystemService(ConnectivityManager::class.java)
        ...
    }
  • 这通常位于单例中,因为它会被多个屏幕甚至多个应用组件(例如服务)读取。

  • 但这并不意味着您不能使用 Compose State 来存储它。

  • 事实上,它通常比基于流程的版本更简单。

  • 您不需要回调流程或任何其他操作来完成适配,只需直接在回调中更新点值即可。

    @Singleton
    class NetworkMonitor @Inject constructor(...) {
        ...
        private val _isOnline = mutableStateOf(checkCurrentConnectivity())
        val isOnline: State<Boolean> = _isOnline
    }
  • 如果消费者仍需要使用 flow,您可以使用 snapshotFlow 桥接回 flow。

    @Singleton
    class NetworkMonitor @Inject constructor(...) {
        ...
        private val _isOnline = mutableStateOf(checkCurrentConnectivity())
        val isOnline: State<Boolean> = _isOnline
    
        private val callback = object : ConnectivityManager.NetworkCallback() {
            override fun onAvailable(network: Network) { _isOnline.value = true }
            override fun onLost(network: Network) { _isOnline.value = false }
        }
    
        init {
            connectivityManager.registerDefaultNetworkCallback(callback)
        }
    }
  • 我们来看看 ViewModel 中的具体情况。

  • 您可以直接在 Transform 中读取 isOnline,当值发生变化时,它会重组。

    val tasksState: State<TaskListState> = transform(
        initialValue = TaskListState.Loading,
        scope = viewModelScope,
    ) {
        if (!networkMonitor.isOnline.value) {
            return@transform TaskListState.Offline
        }
    
        val tasks = remember(searchQuery) {
            tasksRepository.getTasks(searchQuery)
        }.collectAsState(initial = null).value
            ?: return@transform TaskListState.Loading
    
        TaskListState.Success(tasks = tasks)
    }
  • 而且,所有这些都是直接发生的,不需要提供应用范围或协程范围来更新状态,只是直接、同步的更新。

  • 因此,借助 Transform,您的 ViewModel 可以在单个 Transform 代码块中组合来自单例管理器的 State、ViewModel 中的 State 以及来自代码库的 Flow。

  • 现在,我们回到瞬态。

  • 之前,我提到了两种类型:界面状态和会话状态。

  • 对于界面状态,甚至单个 activity 内的会话状态,ViewModel 和 SavedState 都非常适用。

  • 这正是它们的设计初衷。

  • 但是,当需要在多个不同的界面和应用组件之间共享该会话状态时,问题就会开始出现。

  • 一旦该状态移出 ViewModel,我们现有的 API 和指南就会出现一些空白。

  • 因此,首先 SavedState API 会保存到 Activity 的 Bundle 中,但该 Bundle 在其他应用组件(例如服务)中不可用。

  • 第二种方法是将 mutableStateOf 存储在单例中,该单例仅驻留在内存中,因此在进程终止后将失效。

StateStore 简介

  • 而这正是 Jetpack AppState StateStore 填补的空白。

  • 它是 Compose 状态的类型安全映射,可在应用组件之间共享,并由 DataStore 提供支持,以便在进程终止后继续存在。

  • 您可以自行将 DataStore 连接到每个单例,但这意味着要为每个属性编写保存和恢复样板。

  • 借助 StateStore DataStore 集成,您只需连接 DataStore 一次,然后选择要保留的每个状态。

  • 现在,我们就在专注计时器应用中构建该功能。

  • 我们将首先从内存开始,然后再添加 DataStore 持久性。

  • 首先,我们将 FocusSession 建模为一个具有空闲、运行和暂停状态的密封接口。

    sealed interface FocusSession {
        data object Idle : FocusSession
        data class Running(val taskId: String, val endTimeMillis: Long) : FocusSession
        data class Paused(val taskId: String, val remainingMillis: Long) : FocusSession
    }
  • 然后,为了将此状态存储在 StateStore 中,我们定义了一个 StateStoreKey,其类型为 FocusSession,默认值为 idle。

    sealed interface FocusSession { ... }
    
    object FocusSessionKey :
        StateStoreKey<FocusSession>(defaultValue = FocusSession.Idle)
  • 然后,我们在 Hilt 中提供一个单例 StateStore,可以将其注入到需要它的不同组件中,例如 Focus Session Manager。

    @Module
    @InstallIn(SingletonComponent::class)
    object AppModule {
        @Provides
        @Singleton
        fun provideStateStore(): StateStore = StateStore()
    }
    
    @Singleton
    class FocusSessionManager @Inject constructor(
        private val stateStore: StateStore,
    ) {
        ...
    }
  • 这与 ViewModel 注入 SavedStateHandle 的方式类似。

  • 好的。

  • 现在,我们已经有了 StateStore,但该如何使用它呢?

  • 好在它是一个小型 API。

  • 它只有三个方法,因此我们将逐一介绍。

  • 第一个是 setState,它会为键写入新值。

    fun startFocus(taskId: String, durationMinutes: Int = 25) {
        val endTime = System.currentTimeMillis() + durationMinutes * 60_000L
        stateStore.setState(
            FocusSessionKey,
            FocusSession.Running(taskId = taskId, endTimeMillis = endTime),
        )
    }
    
    fun endFocus() {
        stateStore.setState(FocusSessionKey, FocusSession.Idle)
    }
  • 当会话开始时,我们将其设置为“正在运行”;当会话结束时,我们将其设置为“空闲”。

  • 就是这么简单

  • 其次,我们仍然有 updateState,它可让您根据当前值计算新值,例如在运行和暂停之间切换时计算剩余时间。

    fun pauseFocus(nowMillis: Long = System.currentTimeMillis()) {
        stateStore.updateState(FocusSessionKey) { session ->
            if (session !is FocusSession.Running) return@updateState session
            FocusSession.Paused(session.taskId, session.endTimeMillis - nowMillis)
        }
    }
    
    fun resumeFocus(nowMillis: Long = System.currentTimeMillis()) {
        stateStore.updateState(FocusSessionKey) { session ->
            if (session !is FocusSession.Paused) return@updateState session
            FocusSession.Running(session.taskId, nowMillis + session.remainingMillis)
        }
    }
  • 最后是 getState,它会返回键的只读 State,并且可以选择替换默认值,以便在特定调用位置更改该值。

    @Singleton
    class FocusSessionManager @Inject constructor(
        private val stateStore: StateStore,
    ) {
        val session: State<FocusSession> = stateStore.getState(FocusSessionKey)
        ...
    }
  • 此外,在此管理器中公开 FocusSession 状态意味着使用方永远不必直接访问 StateStore 或其键。

  • 您可以维护写入和读取封装。

  • 然后,他们只需注入 Focus Session Manager(他们可能已经有了,因为他们需要控制会话),而不是使用方,然后他们就可以读取 State。

    @HiltViewModel
    class MiniTimerViewModel @Inject constructor(
        focusSessionManager: FocusSessionManager,
    ) : ViewModel() { ... }
    
    @EntryPoint
    @InstallIn(SingletonComponent::class)
    interface WidgetEntryPoint {
        fun focusSessionManager(): FocusSessionManager
    }
  • 然后,这些读取器可以直接重新公开 State,也可以使用 Transform 创建新的、更新后的 State,就像我们之前看到的 Mini-Timer ViewModel 示例一样。

    @HiltViewModel
    class MiniTimerViewModel @Inject constructor(
        focusSessionManager: FocusSessionManager,
    ) : ViewModel() {
        val session: State<FocusSession> = focusSessionManager.session
    }
  • 然后,界面可组合项可以像读取任何其他 Compose 状态一样读取该状态,并在必要时进行重组。

    @HiltViewModel
    class MiniTimerViewModel @Inject constructor(
        focusSessionManager: FocusSessionManager,
    ) : ViewModel() {
        val session: State<FocusSession> = focusSessionManager.session
    }
    
    @Composable
    fun MiniTimerBar(viewModel: MiniTimerViewModel = hiltViewModel()) {
        val session by viewModel.session
        if (session !is FocusSession.Idle) {
            TimerReadout(session = session)
        }
    }
  • 但到目前为止,FocusSession 仅存在于内存中。

  • 那么,我们来看看如何才能在进程终止后恢复。

  • 因此,首先,我们必须在创建存储区时启动 syncToDataStore,以进行一些一次性设置。

    @Module
    @InstallIn(SingletonComponent::class)
    object AppModule {
        @Provides
        @Singleton
        fun provideStateStore(
            @ApplicationContext context: Context,
            @ApplicationScope scope: CoroutineScope,
        ): StateStore = StateStore().apply {
            val path = context.dataStoreFile("app_state.json").absolutePath
            scope.launch { syncToDataStore(path = path, coroutineScope = scope) }
        }
    }
  • 然后,我们选择启用要保留的每个密钥。

    @Serializable
    sealed interface FocusSession {
        @Serializable data object Idle : FocusSession
        @Serializable data class Running(val taskId: String, val endTimeMillis: Long) : FocusSession
        @Serializable data class Paused(val taskId: String, val remainingMillis: Long) : FocusSession
    }
    
    object FocusSessionKey :
        StateStoreKey<FocusSession>(defaultValue = FocusSession.Idle),
        PersistToDataStore
  • 我们只需确保该类型可序列化,并且键实现了 PersistToDataStore 接口。

  • 然后,您可能会想,好吧,这似乎很简单。

  • 那么,它实际上是如何持久保存密钥的呢?

  • 因此,在后台,syncToDataStore 使用 Transform 的监听器 API,该 API 与 Transform 基本完全相同,只是它不返回任何内容,仅用于观察和副作用。

    suspend fun StateStore.syncToDataStore(dataStore: DataStore<StateStorePreferences>) {
        listener { … }
    }
  • 然后,在该监听器中,它会读取存储区中的每个键,并检查其中是否有 PersistToDataStore 键。

    suspend fun StateStore.syncToDataStore(dataStore: DataStore<StateStorePreferences>) {
        listener {
            for (key in keys) {
                if (key is PersistToDataStore) {
                    val value = getState(key).value
    
                    LaunchedEffect(key) {
                        // Restore saved value from dataStore on first access
                    }
    
                    LaunchedEffect(key, value) {
                        // Flush updated value to dataStore on change
                    }
                }
            }
        }
    }
  • 如果已启用,则每当这些值发生变化时,它都会使用 LaunchedEffect 将这些值同步回 DataStore。

  • 在这里,我想指出的是,正是因为有了 StateStore 这样的标准容器,我们才能提供这些即插即用的集成。

  • DataStore只是我们首先要介绍的一个。

Compose State 类别摘要

  • 现在,我们回到状态类别,您应该已经非常清楚如何在整个应用中使用 Compose 状态了。

  • 从用于实时状态的 mutableStateOf,到用于屏幕本地呈现状态的 SavedState API,再到用于跨组件会话状态的 StateStore,最后还有用于与 Flow 互操作的 collectAsState。

  • 但实际上,互操作性并不局限于任何单一类别,只要您需要将可观察类型桥接到 Compose State,就可以使用 collectAsState 等辅助函数。

  • 但对于永久状态,我们正在研究的另一件事是与 Compose 合作,或者让 DataStore 和 Room 直接公开 Compose 状态,这样您就不必编写自己的代码,比如使用更简洁的语法来实现这一点。

  • 因此,我很高兴地宣布,Transform、StateStore 和 DataStore 集成现已推出 Alpha 版,我们还在 developer.android.com 上发布了所有文档,并在 GitHub 上提供了一个 AppState Recipes 代码库,其中包含使用 StateStore 和 Transform 的示例。

    [versions]
    appstate = "1.0.0-alpha01"
    ...
    
    [libraries]
    ...
    transform = { module = "androidx.appstate.transform:transform", version.ref = "appstate" }
    transform-testing = { module = "androidx.appstate.transform:transform-testing", version.ref = "appstate" }
    statestore = { module = "androidx.appstate.statestore:statestore", version.ref = "appstate" }
    statestore-datastore = { module = "androidx.appstate.statestore:statestore-datastore", version.ref = "appstate" }
  • 请在您的应用中试用这些功能。

  • 欢迎提供反馈。

  • 比如,我们现在处于 Alpha 版阶段,所以我们现在可以真正进行迭代和做出更改,以确保这些更改适用于您的用例。

代码

代码段

“什么是 Compose 状态?”中 01:56 处的 Kotlin 代码段
/**
 * A value holder where reads to the [value] property during the execution of a [Composable]
 * function, the current [RecomposeScope] will be subscribed to changes of that value.
 */
@Stable
public interface State<out T> {
    public val value: T
}
“什么是 Compose 状态?”中 02:00 处的 Kotlin 代码段
/**
 * A mutable value holder where reads to the [value] property during the execution of a [Composable]
 * function, the current [RecomposeScope] will be subscribed to changes of that value. When the
 * [value] property is written to and changed, a recomposition of any subscribed [RecomposeScope]s
 * will be scheduled. If [value] is written to with the same value, no recompositions will be
 * scheduled.
 */
@Stable
public interface MutableState<T> : State<T> {
    override var value: T
    public operator fun component1(): T
    public operator fun component2(): (T) -> Unit
}
“什么是 Compose 状态?”视频中 02:23 处的 Kotlin 代码段
@Composable
fun Counter() {
    val count = remember { mutableStateOf(0) }
}
“什么是 Compose 状态?”中 02:32 处的 Kotlin 代码段
@Composable
fun Counter() {
    val count = remember { mutableStateOf(0) }
    Column {
        Text("Count: ${count.value}")
        AddButton(onClick = { count.value++ })
    }
}
“什么是 Compose 状态?”中 02:52 处的 Kotlin 代码段
@Composable
fun Counter() {
    val count = remember { mutableStateOf(0) }
    Column {
        Text("Count: ${count.value}")
        AddButton(onClick = { count.value++ })
    }
}

@Composable
fun AddButton(onClick: () -> Unit) {
    Button(onClick = onClick) {
        Text("Add")
    }
}
“什么是 Compose 状态?”中 03:08 处的 Kotlin 代码段
@Composable
fun Counter() {
    val isVisible = remember { mutableStateOf(true) }
    val count = remember { mutableStateOf(0) }
    Column {
        if (isVisible.value) {
            Text("Count: ${count.value}")
        } else {
            Text("Hidden")
        }
        AddButton(onClick = { count.value++ })
    }
}
“什么是 Compose 状态?”视频中 03:26 处的 Kotlin 代码段
@Composable
fun Counter() {
    val isVisible = remember { mutableStateOf(false) }
    val count = remember { mutableStateOf(0) }
    Column {
        if (isVisible.value) {
            Text("Count: ${count.value}")
        } else {
            Text("Hidden")
        }
        AddButton(onClick = { count.value++ })
    }
}
“什么是 Compose 状态?”中 04:15 处的 Kotlin 代码段
val levelName: String = "Level 1 - Stay Alive!"
val timeSurvived: Int = 0
val score: Double = 0.0
“什么是 Compose 状态?”视频中 04:23 处的 Kotlin 代码段
data class GameUiState(
    val levelName: String = "Level 1 - Stay Alive!",
    val timeSurvived: Int = 0,
    val score: Double = 0.0
)
“什么是 Compose 状态?”中 04:33 处的 Kotlin 代码段
data class GameUiState(
    val levelName: String = "Level 1 - Stay Alive!",
    val timeSurvived: Int = 0,
    val score: Double = 0.0
)

class GameViewModel : ViewModel() {
    val uiState = MutableStateFlow(GameUiState())
    fun updateScore() = uiState.update {
        it.copy(
            timeSurvived = it.timeSurvived + 1,
            score = it.score + 10 + .01 * it.timeSurvived
        )
    }
}
“什么是 Compose 状态?”中 04:42 处的 Kotlin 代码段
data class GameUiState(
    val levelName: String = "Level 1 - Stay Alive!",
    val timeSurvived: Int = 0,
    val score: Double = 0.0
)

class GameViewModel : ViewModel() {
    val uiState = MutableStateFlow(GameUiState())
    fun updateScore() = uiState.update {
        it.copy(
            timeSurvived = it.timeSurvived + 1,
            score = it.score + 10 + .01 * it.timeSurvived
        )
    }
}

@Composable
fun GameScreen(viewModel: GameViewModel = viewModel()) {
    val state by viewModel.uiState.collectAsStateWithLifecycle()
    Column {
        Header(levelName = state.levelName)
        ScoreBadge(
            timeSurvived = state.timeSurvived,
            score = state.score
        )
    }
}
“什么是 Compose 状态?”中 05:09 处的 Kotlin 代码段
@Stable
class GameUiState {
    var levelName by mutableStateOf("Level 1 - Stay Alive!")
    var timeSurvived by mutableStateOf(0)
    var score by mutableStateOf(0.0)
}
“什么是 Compose 状态?”中 05:21 处的 Kotlin 代码段
@Stable
class GameUiState {
    var levelName by mutableStateOf("Level 1 - Stay Alive!")
    var timeSurvived by mutableStateOf(0)
    var score by mutableStateOf(0.0)
}

class GameViewModel : ViewModel() {
    val uiState = GameUiState()
    fun updateScore() = Snapshot.withMutableSnapshot {
        uiState.timeSurvived += 1
        uiState.score += 10 + .01 * uiState.timeSurvived
    }
}
“什么是 Compose 状态?”中 05:30 处的 Kotlin 代码段
@Stable
class GameUiState {
    var levelName by mutableStateOf("Level 1 - Stay Alive!")
    var timeSurvived by mutableStateOf(0)
    var score by mutableStateOf(0.0)
}

class GameViewModel : ViewModel() {
    val uiState = GameUiState()
    fun updateScore() = Snapshot.withMutableSnapshot {
        uiState.timeSurvived += 1
        uiState.score += 10 + .01 * uiState.timeSurvived
    }
}

@Composable
fun GameScreen(viewModel: GameViewModel = viewModel()) {
    val state = viewModel.uiState
    Column {
        Header(state = state)
        ScoreBadge(state = state)
    }
}
“Introducing Transform”(Transform 简介)中 07:35 处的 Kotlin 代码段
public fun <R> transform(
    initialValue: R,
    scope: CoroutineScope,
    onUpdate: @Composable (R) -> R,
): State<R>
“Introducing Transform”(Transform 简介)中 09:20 处的 Kotlin 代码段
class FocusScreenViewModel(
    focusManager: FocusSessionManager,
    taskRepository: TaskRepository
) : ViewModel() {
    val summary: StateFlow<FocusSummary> =
        combine(focusManager.sessionFlow, taskRepository.tasksFlow) { session, tasks ->
            if (session.phase == FocusPhase.Idle) {
                FocusSummary.Idle
            } else {
                val task = tasks.find { it.id == session.activeTaskId }
                FocusSummary.Active(...)
            }
        }.stateIn(
                scope = viewModelScope,
        started = SharingStarted.WhileSubscribed(5_000),
        initialValue = FocusSummary.Idle,
)
}
“Introducing Transform”(Transform 简介)中 09:37 处的 Kotlin 代码段
class FocusScreenViewModel(
    focusManager: FocusSessionManager,
    taskRepository: TaskRepository
) : ViewModel() {
    val summary: State<FocusSummary> =
        transform<FocusSummary>(
initialValue = FocusSummary.Idle,
scope = viewModelScope,
container = handle.getOrCreateContainer("transform")
) {
            val session by focusManager.sessionFlow.collectAsState()
            if (session.phase == FocusPhase.Idle) return@transform FocusSummary.Idle
            val tasks by taskRepository.tasksFlow.collectAsState(emptyList())
            val task = tasks.find { it.id == session.activeTaskId }
            FocusSummary.Active(...)
“Introducing Transform”(Transform 简介)视频中 10:01 处的 Kotlin 代码段
class FocusScreenViewModel(
    focusManager: FocusSessionManager,
    taskRepository: TaskRepository
) : ViewModel() {
    val summary: State<FocusSummary> =
        transform<FocusSummary>(
            initialValue = FocusSummary.Idle,
            scope = viewModelScope,
                   container = handle.getOrCreateContainer("transform")
        ) {
            val session by focusManager.sessionFlow.collectAsState()
            if (session.phase == FocusPhase.Idle)
                return@transform FocusSummary.Idle
            val tasks by taskRepository.tasksFlow.collectAsState(emptyList())
            val task = tasks.find {
                it.id == session.activeTaskId
            }
                     FocusSummary.Active(...)
        }
}
“嵌套转换”中 11:39 处的 Kotlin 代码段
class FocusSessionManager(taskRepository: TaskRepository, scope: CoroutineScope) {
    ...
}

class MiniTimerViewModel(focusManager: FocusSessionManager) : ViewModel() {
    ...
}

class FocusWidget : GlanceAppWidget() {
    override suspend fun provideGlance(context: Context, id: GlanceId) {
        provideContent {
            ...
        }
    }
}
“嵌套转换”中 12:03 处的 Kotlin 代码段
class FocusSessionManager(
    taskRepository: TaskRepository,
    scope: CoroutineScope
) {
    val sessionFlow: StateFlow<FocusSession> = ...
    val summary: StateFlow<FocusSummary> =
        combine(
            sessionFlow,
            taskRepository.tasksFlow
        ) { session, tasks ->
            if (session.phase == FocusPhase.Idle) {
                FocusSummary.Idle
            } else {
                val task = tasks.find {
                    it.id == session.activeTaskId
                }
                FocusSummary.Active(...)
            }
        }.stateIn(
scope = appScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = FocusSummary.Idle
)
}
“嵌套转换”中 12:17 处的 Kotlin 代码段
class FocusSessionManager(
    taskRepository: TaskRepository,
    scope: CoroutineScope
) {
    val sessionFlow: StateFlow<FocusSession> = ...
    val summary: State<FocusSummary> =
        transform<FocusSummary>(
           initialValue = FocusSummary.Idle,
scope = scope,
container = handle.getOrCreateContainer("transform")
        ) {
            val session by sessionFlow.collectAsState()
            if (session.phase == FocusPhase.Idle)
                return@transform FocusSummary.Idle
            val tasks by taskRepository.tasksFlow
                .collectAsState(emptyList())
            val task = tasks.find {
                it.id == session.activeTaskId
            }
            FocusSummary.Active(...)
        }
}
“嵌套转换”中 12:28 处的 Kotlin 代码段
class MiniTimerViewModel(
    focusManager: FocusSessionManager
) : ViewModel() {
    val isExpanded = MutableStateFlow(false)

    val uiState: StateFlow<MiniTimerUiState> =
        combine(
            focusManager.summary,
            isExpanded
        ) { summary, expanded ->
            MiniTimerUiState(
    summary = summary,
    showControls = expanded && summary is FocusSummary.Active
)
        }.stateIn(
        scope = viewModelScope,
        started = SharingStarted.WhileSubscribed(5_000),
        initialValue = MiniTimerUiState()
)

    fun toggleExpanded() { isExpanded.update { !it } }
}
“嵌套转换”中 12:56 处的 Kotlin 代码段
class MiniTimerViewModel(
    focusManager: FocusSessionManager
) : ViewModel() {
    val isExpanded = MutableStateFlow(false)

    val uiState: State<MiniTimerUiState> =
        transform(
            initialValue = MiniTimerUiState(),
            scope = viewModelScope,
                   container = handle.getOrCreateContainer("transform")
        ) {
            val summary by focusManager.summary
                   val expanded by isExpanded.collectAsState()
            MiniTimerUiState(
    summary = summary,
    showControls = expanded && summary is FocusSummary.Active
)
        }

    fun toggleExpanded() { isExpanded.update { !it } }
}
“嵌套转换”中 13:37 处的 Kotlin 代码段
class FocusWidget : GlanceAppWidget() {
    override suspend fun provideGlance(
        context: Context,
        id: GlanceId
    ) {
        val focusManager = ...
        provideContent {
            val summary by focusManager.summary
                .collectAsState()
            GlanceTheme { FocusWidgetContent(summary) }
        }
    }
}
“嵌套转换”中 13:47 处的 Kotlin 代码段
class FocusWidget : GlanceAppWidget() {
    override suspend fun provideGlance(
        context: Context,
        id: GlanceId
    ) {
        val focusManager = ...
        provideContent {
            val summary by focusManager.summary
            GlanceTheme { FocusWidgetContent(summary) }
        }
    }
}
“嵌套转换”中 14:33 处的 Kotlin 代码段
public fun <R> transform(
    initialValue: R,
    scope: CoroutineScope,
    onUpdate: @Composable (R) -> R,
): State<R> {
        val transformScope = TransformScope()
    val transform = transformScope
.launchTransform(initialValue, onUpdate)
    scope.launch(
dispatcher,
start = CoroutineStart.UNDISPATCHED
 ) {
        try { transformScope.run() } finally {
            transform.dispose()
        }
    }
    return transform
}
“嵌套转换”中 15:18 处的 Kotlin 代码段
public fun TransformScope(context: CoroutineContext = Dispatchers.Default): TransformScope

public abstract class TransformScope {
        public fun <T> createTransform(initialState: T, onUpdate: @Composable (T) -> T): DisposableState<T>
public abstract suspend fun run()
}

public fun <T> TransformScope.launchTransform(
initialState: T,
onUpdate: @Composable (T) -> T,
): DisposableState<T>

public interface DisposableState<T> : State<T> {
public fun dispose()
}
“嵌套转换”中 16:02 处的 Kotlin 代码段
override suspend fun run() {
        val context = currentCoroutineContext()
        val clock = context[MonotonicFrameClock] ?: HeadlessTransformClock()
        ...
        val recomposer = Recomposer(context)
        val composition = Composition(UnitApplier, recomposer)
        try {
            withContext(clock) {
                if (clock is HeadlessTransformClock) {
                    launch(start = CoroutineStart.UNDISPATCHED) {
                        clock.runClock()
                    }
                }
                ...
            }
        } finally {
            // dispose of composition, cancel the clock, close the recomposer
        }
    }
“界面之外的 Compose 状态”中 19:12 处的 Kotlin 代码段
@HiltViewModel
class TaskListViewModel @Inject constructor(
    private val tasksRepository: TasksRepository,
    private val networkMonitor: NetworkMonitor,
    savedStateHandle: SavedStateHandle,
) : ViewModel() {
    ...
}
“界面之外的 Compose 状态”中 19:17 处的 Kotlin 代码段
@HiltViewModel
class TaskListViewModel @Inject constructor(...) : ViewModel() {
    var selectedTaskIds by mutableStateOf(emptySet<String>())
        private set
}
“界面之外的 Compose 状态”中 19:33 处的 Kotlin 代码段
@HiltViewModel
class TaskListViewModel @Inject constructor(...) : ViewModel() {
    var selectedTaskIds by mutableStateOf(emptySet<String>())
        private set

    var searchQuery by savedStateHandle.saveable { mutableStateOf("") }
        private set
}
“Compose 状态在界面之外”视频中 19:41 处的 Kotlin 代码段
@HiltViewModel
class TaskListViewModel @Inject constructor(...) : 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()
        }
    }
}
“界面之外的 Compose 状态”中 19:52 处的 Kotlin 代码段
val tasksState: State<TaskListState> = transform(
    initialValue = TaskListState.Loading,
    scope = viewModelScope,
) {
    val tasks = remember(searchQuery) {
        tasksRepository.getTasks(searchQuery)
    }.collectAsState(initial = null).value
        ?: return@transform TaskListState.Loading

    TaskListState.Success(tasks = tasks)
}
“界面之外的 Compose 状态”中 20:00 处的 Kotlin 代码段
@Singleton
class NetworkMonitor @Inject constructor(
    @ApplicationContext context: Context,
) {
    private val connectivityManager =
        context.getSystemService(ConnectivityManager::class.java)
    ...
}
“界面之外的 Compose 状态”中 20:17 处的 Kotlin 代码段
@Singleton
class NetworkMonitor @Inject constructor(...) {
    ...
    private val _isOnline = mutableStateOf(checkCurrentConnectivity())
    val isOnline: State<Boolean> = _isOnline
}
“界面之外的 Compose 状态”中 20:23 处的 Kotlin 代码段
@Singleton
class NetworkMonitor @Inject constructor(...) {
    ...
    private val _isOnline = mutableStateOf(checkCurrentConnectivity())
    val isOnline: State<Boolean> = _isOnline

    private val callback = object : ConnectivityManager.NetworkCallback() {
        override fun onAvailable(network: Network) { _isOnline.value = true }
        override fun onLost(network: Network) { _isOnline.value = false }
    }

    init {
        connectivityManager.registerDefaultNetworkCallback(callback)
    }
}
“界面之外的 Compose 状态”视频中 20:32 处的 Kotlin 代码段
val tasksState: State<TaskListState> = transform(
    initialValue = TaskListState.Loading,
    scope = viewModelScope,
) {
    if (!networkMonitor.isOnline.value) {
        return@transform TaskListState.Offline
    }

    val tasks = remember(searchQuery) {
        tasksRepository.getTasks(searchQuery)
    }.collectAsState(initial = null).value
        ?: return@transform TaskListState.Loading

    TaskListState.Success(tasks = tasks)
}
“StateStore 简介”中 22:07 处的 Kotlin 代码段
sealed interface FocusSession {
    data object Idle : FocusSession
    data class Running(val taskId: String, val endTimeMillis: Long) : FocusSession
    data class Paused(val taskId: String, val remainingMillis: Long) : FocusSession
}
“StateStore 简介”中 22:14 处的 Kotlin 代码段
sealed interface FocusSession { ... }

object FocusSessionKey :
    StateStoreKey<FocusSession>(defaultValue = FocusSession.Idle)
“StateStore 简介”中 22:23 处的 Kotlin 代码段
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
    @Provides
    @Singleton
    fun provideStateStore(): StateStore = StateStore()
}

@Singleton
class FocusSessionManager @Inject constructor(
    private val stateStore: StateStore,
) {
    ...
}
“StateStore 简介”中 22:42 处的 Kotlin 代码段
fun startFocus(taskId: String, durationMinutes: Int = 25) {
    val endTime = System.currentTimeMillis() + durationMinutes * 60_000L
    stateStore.setState(
        FocusSessionKey,
        FocusSession.Running(taskId = taskId, endTimeMillis = endTime),
    )
}

fun endFocus() {
    stateStore.setState(FocusSessionKey, FocusSession.Idle)
}
“StateStore 简介”中 22:52 处的 Kotlin 代码段
fun pauseFocus(nowMillis: Long = System.currentTimeMillis()) {
    stateStore.updateState(FocusSessionKey) { session ->
        if (session !is FocusSession.Running) return@updateState session
        FocusSession.Paused(session.taskId, session.endTimeMillis - nowMillis)
    }
}

fun resumeFocus(nowMillis: Long = System.currentTimeMillis()) {
    stateStore.updateState(FocusSessionKey) { session ->
        if (session !is FocusSession.Paused) return@updateState session
        FocusSession.Running(session.taskId, nowMillis + session.remainingMillis)
    }
}
“StateStore 简介”中 23:03 处的 Kotlin 代码段
@Singleton
class FocusSessionManager @Inject constructor(
    private val stateStore: StateStore,
) {
    val session: State<FocusSession> = stateStore.getState(FocusSessionKey)
    ...
}
“StateStore 简介”中 23:22 处的 Kotlin 代码段
@HiltViewModel
class MiniTimerViewModel @Inject constructor(
    focusSessionManager: FocusSessionManager,
) : ViewModel() { ... }

@EntryPoint
@InstallIn(SingletonComponent::class)
interface WidgetEntryPoint {
    fun focusSessionManager(): FocusSessionManager
}
“StateStore 简介”中 23:33 处的 Kotlin 代码段
@HiltViewModel
class MiniTimerViewModel @Inject constructor(
    focusSessionManager: FocusSessionManager,
) : ViewModel() {
    val session: State<FocusSession> = focusSessionManager.session
}
“StateStore 简介”中 23:45 处的 Kotlin 代码段
@HiltViewModel
class MiniTimerViewModel @Inject constructor(
    focusSessionManager: FocusSessionManager,
) : ViewModel() {
    val session: State<FocusSession> = focusSessionManager.session
}

@Composable
fun MiniTimerBar(viewModel: MiniTimerViewModel = hiltViewModel()) {
    val session by viewModel.session
    if (session !is FocusSession.Idle) {
        TimerReadout(session = session)
    }
}
“StateStore 简介”中 23:58 处的 Kotlin 代码段
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
    @Provides
    @Singleton
    fun provideStateStore(
        @ApplicationContext context: Context,
        @ApplicationScope scope: CoroutineScope,
    ): StateStore = StateStore().apply {
        val path = context.dataStoreFile("app_state.json").absolutePath
        scope.launch { syncToDataStore(path = path, coroutineScope = scope) }
    }
}
“StateStore 简介”中 24:04 处的 Kotlin 代码段
@Serializable
sealed interface FocusSession {
    @Serializable data object Idle : FocusSession
    @Serializable data class Running(val taskId: String, val endTimeMillis: Long) : FocusSession
    @Serializable data class Paused(val taskId: String, val remainingMillis: Long) : FocusSession
}

object FocusSessionKey :
    StateStoreKey<FocusSession>(defaultValue = FocusSession.Idle),
    PersistToDataStore
“StateStore 简介”中 24:22 处的 Kotlin 代码段
suspend fun StateStore.syncToDataStore(dataStore: DataStore<StateStorePreferences>) {
    listener { … }
}
“StateStore 简介”中 24:33 处的 Kotlin 代码段
suspend fun StateStore.syncToDataStore(dataStore: DataStore<StateStorePreferences>) {
    listener {
        for (key in keys) {
            if (key is PersistToDataStore) {
                val value = getState(key).value

                LaunchedEffect(key) {
                    // Restore saved value from dataStore on first access
                }

                LaunchedEffect(key, value) {
                    // Flush updated value to dataStore on change
                }
            }
        }
    }
}
“Compose 状态类别摘要”中 25:39 处的 TOML 代码段
[versions]
appstate = "1.0.0-alpha01"
...

[libraries]
...
transform = { module = "androidx.appstate.transform:transform", version.ref = "appstate" }
transform-testing = { module = "androidx.appstate.transform:transform-testing", version.ref = "appstate" }
statestore = { module = "androidx.appstate.statestore:statestore", version.ref = "appstate" }
statestore-datastore = { module = "androidx.appstate.statestore:statestore-datastore", version.ref = "appstate" }