Jetpack AppState:让Compose状态走出UI层

Jetpack AppState 将 Compose snapshot 状态机制延伸到数据层,推出 transform 与 State Store 两个库。
文章介绍 Google 提出的 Jetpack AppState,其核心思路是将 Compose 的 snapshot 状态机制从 UI 层延伸到数据层与领域层。Compose 由 Compose UI 与独立的 runtime 构成,snapshot 状态具备自动动态读取追踪以及一致性与粒度兼顾两大特性,可脱离 UI 使用。基于此,transform 库通过无头组合把数据模型写成 composable 代码块,读取即订阅,支持提前返回、嵌套 transform 与 eager 的 transform frame clock;State Store 则是类型安全、可跨组件共享并由 DataStore 持久化的 Compose 状态映射,填补 SavedStateHandle 与内存单例之间的空白。两个库均已进入 alpha 阶段。
Compose 常被视为一个 UI 工具包,但它的底层其实由两层构成:负责布局、绘制与输入的 Compose UI,以及一个与 UI 完全解耦、专注状态管理与重组的 Compose runtime。Google 在 Jetpack 架构库团队的技术分享中提出的 Jetpack AppState,正是基于这一认知,试图把 Compose 的 snapshot 状态机制从界面层延伸到数据层与领域层,解决 Android 应用中两个反复出现的痛点:复杂状态的编排,以及跨组件状态共享与存活。
为什么 Compose 状态值得被带出 UI
据 Google 开发者关系工程师 Ben Walters 与架构库工程师 Elif 的介绍,随着应用横跨手机、平板、小组件、手表、电视与 XR 设备,UI 背后的状态管理会迅速变难:一方面要把多个数据流、本地 UI 输入和环境状态(如网络连通性)编织在一起,往往堆满 combine、flatMapLatest、distinctUntilChanged 之类的操作符;另一方面要让同一份状态在 Activity、Service、App Widget 之间保持同步,并在需要时跨进程死亡存活。
Compose 的 snapshot 状态恰好提供了流(Flow)不具备的两个特性。其一是自动且动态的读取追踪:不需要订阅或收集,读取 .value 本身就是订阅;且追踪发生在运行时,只有实际执行的代码路径才会被登记为读取者。其二是同时兼顾一致性与粒度:在 snapshot 内更新多个值会原子生效,而不同属性可以独立追踪,更新其中一个只让真正依赖它的读取者失效。

演讲用一个游戏状态示例说明了差异:把等级、存活时间、分数打包进一个 UI 状态类并用 Flow 实现,每次更新分数都要复制整个状态对象,导致只读部分数据的界面也被无谓地失效;改用委托到 mutable state 的实现,并用 withMutableSnapshot 包裹写入,界面把状态直接透传给子组件而不读取,最终只有真正读取分数的组件被重组。演讲者强调,这套机制并不绑定 UI,UI 只是它最显眼的用武之地。
Compose 的 snapshot 状态机制源于其运行时对状态读取与写入的追踪模型。在 snapshot 系统中,每个状态对象(MutableState)的读取会登记到当前活跃的 snapshot 中,写入则先暂存于 snapshot 副本,直到显式提交(Snapshot.apply 或 withMutableSnapshot)才原子地生效。这一设计允许多个 snapshot 并发存在而互不干扰,同时通过全局的写入观察者(Snapshot.registerApplyObserver)通知依赖方失效。与 Flow 的冷流模型不同,snapshot 状态不需要显式的订阅关系,读取行为本身即构成依赖登记;且由于追踪发生在运行时,只有实际执行到的代码路径才会被记录为读取者,这使得条件分支下的依赖关系天然精确。正是这种「读取即订阅、写入即原子提交」的语义,让 snapshot 状态在一致性、粒度与动态性上区别于传统响应式流,也成为其能够脱离 UI 层复用的根本原因。
Transform:用 Compose 运行时驱动数据模型
Jetpack AppState 的第一个库是 transform,它回答的问题是:如果数据模型本身就是状态,该如何为所有消费者计算和派生状态。这一思路并非凭空而来——Molecule 库已经证明,composable 函数不必发射 UI 节点,而可以通过无头组合(headless composition)产出纯数据模型。transform 把这一点与状态模型结合:数据模型直接是状态。

据 Elif 介绍,transform 的 API 只有一个函数,返回一个普通的可观察状态,而不是 Flow 或 StateFlow,消费者无从知晓它由 transform 产生。关键在于其代码块是 composable 的,因此 LaunchedEffect、collectAsState、提前返回、普通 if 分支等一切在 composable 中可用的能力都能直接使用。
一个专注计时器示例展示了它的价值:会话状态来自每秒跳动的 timer 引擎,当前任务来自 Room 数据库的 Flow,而 ViewModel 需要把两者合成一个摘要。传统写法要用 combine 串联两个流、在 lambda 里做分支判断,最后 stateIn;transform 则把整个派生过程写成代码块本身,用 collectAsState 桥接各个 Flow,读取即订阅。由于动态读取追踪,当处于 idle 状态时,Room 的冷流根本不会被收集,提前返回让不必要的处理被直接跳过。
无头组合(headless composition)是指在不创建任何 UI 视图树的情况下运行 Compose 的组合过程。Compose 运行时本身并不依赖 Android View 系统,Recomposer 与 Composition 可以在纯 JVM 环境中启动,组合出的结果不是界面节点,而是任意数据对象。Molecule 库最早将这一能力产品化:它暴露一个返回 StateFlow 的入口,在后台协程中驱动组合,使 LaunchedEffect、remember、collectAsState 等 composable 原语可被用于构建数据管道。由于没有窗口与显示帧,无头组合需要自行提供帧时钟来驱动重组,这正是 transform 实现 transform frame clock 的动机。无头组合的意义在于复用 Compose 的声明式编程模型与精确的读取追踪,而无需引入 UI 框架的其余部分。
嵌套 transform 与跨界面状态一致
当同一份会话摘要需要出现在迷你计时条和桌面小组件上时,为每个界面各写一遍派生逻辑意味着三处代码必须彼此保持一致。更合理的做法是在应用作用域派生一次,下游全部消费同一份状态。

在传统实现中,迷你计时条 ViewModel 还要额外追踪一个表示展开与否的状态,于是又叠加一层 combine 与 stateIn。transform 允许嵌套:ViewModel 把上游 transform 的产出当作普通状态读取,同时用 collectAsState 收集本地状态,二者混在同一个块里。利用粒度特性,切换展开状态只会重跑这一小块,应用级派生与小组件完全不受影响。由于迷你计时条和小组件读取的是同一份状态,二者永远不会不一致;在小组件上点击暂停,变更会经由管理器、会话流、摘要回流,三个界面同步更新。
底层上,transform 会创建 transform scope,在其中启动可组合的变换并返回可释放状态,scope 结束时自动释放变换。由于是无头环境,没有显示帧时钟可用,团队专门实现了 transform frame clock:它采用 eager 策略,被消费的状态一变化就立刻产出新帧;空闲时时钟停驻,未被读取的状态不产生任何开销。该机制是一个无锁状态机,请求帧只是一次 compare-and-set。此外 transform 现已支持 rememberSaveable,通过新的 saveStateContainer API 实现配置变更后的数据保存,而该 API 也是 Compose 可保存状态注册表、SavedStateHandle 与 SavedStateRegistry 的共同基础。
State Store:跨越组件边界的状态容器
Ben 把应用中的状态分为三类:仅存在于内存的实时状态(如网络或蓝牙连接状态)、需要跨进程死亡存活但无需长期存储的瞬态状态(如 UI 展开状态、导航返回栈、表单草稿)、以及长期存储的永久状态(如用户偏好或数据库中的媒体库)。

过去架构指南要求 Compose 状态只留在 UI 层或 ViewModel,其他位置一律用 Flow,原因是状态属性一旦需要与 Flow 合并,就得用 snapshotFlow 转回去。有了 transform,这一转换不再必要:本地 UI 输入可以直接声明为 mutableStateOf;需要跨进程死亡的查询则可由 SavedStateHandle.savable 支撑;singleton 中的连接状态也能直接用 Compose 状态存储,在回调里直接更新 .value,需要时再用 snapshotFlow 桥接回 Flow。
真正存在缺口的是需要跨多个界面和组件共享的会话状态:SavedStateHandle 写入的是 Activity 的 bundle,在 Service 等组件中不可用;而 singleton 里的 mutableStateOf 只活在内存中,无法跨进程死亡存活。State Store 正是为此设计——一个类型安全、基于 Compose 状态、可跨组件共享并由 DataStore 持久化的映射。它只有三个方法:setState 写入新值,updateState 基于当前值计算新值(如切换运行与暂停时计算剩余时间),getState 返回只读状态并可在调用点覆盖默认值。
持久化只需一次性把 DataStore 同步到 store,然后让每个需要持久化的键实现相应接口。底层借助 transform 的 listener API(与 transform 相同但不返回值,仅用于观察和副作用),在其中读取所有键并通过 LaunchedEffect 把变化同步回 DataStore。演讲者强调,拥有 State Store 这样的标准容器,才能提供这类开箱即用的集成,DataStore 只是第一个。
演讲结尾宣布,Transform、State Store 与 DataStore 集成均已进入 alpha,文档已发布在 developer.android.com,GitHub 上还有 AppState recipes 仓库提供示例。两位演讲者反复提醒,alpha 阶段正是收集反馈、按真实用例调整 API 的时机。
SavedStateHandle 是 Jetpack ViewModel 提供的键值对容器,用于在进程被系统回收后恢复状态。它底层依赖 SavedStateRegistry,而 SavedStateRegistry 的宿主是 Activity 或 Fragment 等具有生命周期与状态保存回调的组件。在 Activity 中,SavedStateHandle 的内容最终写入该 Activity 的 saved instance state bundle,由系统在配置变更或进程重建时交还。问题在于,Service、App Widget 等组件并不具备同等的 saved state 注册机制,无法直接持有 SavedStateHandle;同时 SavedStateHandle 的作用域限于单个 ViewModel,多个界面或组件之间若要共享同一份瞬态状态,缺乏天然的传播通道。State Store 以 DataStore 作为持久化后端,绕过了组件级的 bundle 机制,从而在 Activity、Service、小组件之间建立统一的、可跨进程死亡存活的共享容器。
编辑观察
AppState 的野心不在于新增一个状态管理框架,而在于把 Compose runtime 从「UI 引擎」重新定义为「通用状态引擎」。对已经全面采用 Compose 的团队,这意味着数据层与 UI 层可以共享同一套状态语义,省去 Flow 与状态之间的反复转换;对仍在混合使用 Flow 架构的项目,transform 提供了渐进迁移的路径。需要留意的是,所有 API 仍处于 alpha,接口随时可能变化,生产环境采用前应评估迁移成本。
相关推荐

零基础用Claude Code搭建AI Agent:从工作区到云端自治
无需编程,用Claude Code桌面应用四步搭建AI Agent:从工作区配置、Agent Designer生成第一个智能体,到Skill固定流程、Memory自我进化,最终部署云端定时自治运行。

无需Python基础:Cursor AI编程如何重塑自动化测试与测试开发
测试工程师不学Python也能做自动化测试?本文梳理Cursor等AI编程工具如何赋能功能测试、自动化脚本、测试平台开发与性能调优,并对比国内外主流工具的成本与访问限制,给出测试人员的能力转型建议。

近5000人中招!AI编程插件被植入后门,官方紧急下架
Claude Code的VS Code扩展2.1.313版本被恶意篡改,植入后门可远程登录并持久化控制,最多5000名用户或受影响。官方已下架恶意版本,本文详解攻击手法与清理步骤。