Runtime 运行时模块
viewcompose-runtime 是 ViewCompose 其他模块共同使用的平台无关状态、快照、观察与轻量组合
引擎。当自定义集成只需要 ViewCompose 的状态或组合语义,而不需要 Android View 宿主时,
可以直接使用该模块。
本模块不负责 UI 渲染、Android 生命周期集成、可视帧调度,也不负责跨进程重建的状态持久化。 这些职责属于更上层的模块及其宿主。
产物与稳定性
dependencies {
implementation("com.viewcompose:viewcompose-runtime:0.1.0-alpha04")
}
- 稳定性:Alpha。Alpha 版本之间可能发生源码和二进制不兼容变更。
- 平台:Kotlin/JVM,使用 Java 11 工具链编译;不依赖 Android SDK 或 AndroidX。
- 直接依赖的 ViewCompose 模块:无。
- 传递提供的 ViewCompose 模块:无。
- 公共
snapshotFlowAPI 返回Flow,因此会暴露 Kotlin Coroutines。 - 本版本构建基线:Kotlin 2.2.10。除非选择的其他产物有要求,否则使用者不需要 Android Gradle Plugin。
最小状态示例
val count = mutableStateOf(0)
val label = derivedStateOf { "Count: ${count.value}" }
count.value += 1
check(label.value == "Count: 1")
显式可变快照之外的状态写入会立即提交。需要让多个值原子可见时,应使用事务:
val count = mutableStateOf(0)
val enabled = mutableStateOf(false)
Snapshot.withMutableSnapshot {
count.value = 1
enabled.value = true
}
check(count.value == 1 && enabled.value)
主要 API
State、MutableState与derivedStateOf提供支持快照的值,以及基于读取依赖惰性计算的派生状态。Snapshot与MutableSnapshot提供一致性读取、带冲突报告的原子缓冲写入。RuntimeObservation是把状态读取转化为显式失效订阅的 Q3 API。一次成功的全局 Apply 最多在 Apply 线程调用每个 受影响 Observation 一次,即使多个依赖同时变化;不同 Apply 仍是不同的通知机会。Q3prepareReplacement会通过同一个 Observation Identity 读取候选依赖集合;提交时保留共有订阅 并原子切换,中止时不会扰动已提交依赖集合。snapshotFlow创建 Cold Flow;它会为每个 Collector 跟踪 Snapshot 读取、合并失效、替换条件依赖,并只发出 结构不相等的计算结果。ComposerLite在不依赖编译器生成变更标记的前提下,提供事务式位置组合、remember 值、effect 与诊断。- Q3
ComposerLite.withReusableContent可以改变可复用结构的逻辑 State Owner,同时保留相等的 纯结构结果。显式 Owner Transfer 只重新执行持有 Remember 值、Saveable Path、Effect 或 Observation 的后代 Group;Prepare 失败会恢复先前已提交的 Owner。 CompositionTimingCollector、CompositionTimingScope与ComposerLite.prepareRootWithTiming组成 Q3、仅请求期有效的组合计时边界。只有实际执行的 Scope 会被提交给 Collector;跳过的 Scope 不调用接口,也不读取时钟。Collector 负责单一 Monotonic Clock、嵌套核算、上限与开销测量;Runtime 提供惰性分配的进程内 Identity 和已经保留的有界 Source Hint。MonotonicFrameClock是动画集成所消费的平台无关计时契约。
可复用内容的 Owner Transfer 是显式操作,因此物理容器可以保留纯结构,而不会继承另一个逻辑 Item 的 Remembered State:
val composer = ComposerLite()
var owner = "account-A"
var revision = 0L
fun compose(replaceOwner: Boolean): Any {
composer.requestRootRecompose()
return composer.composeRoot {
composer.runGroup(
signature = "reusable-host",
inputs = revision,
) {
composer.withReusableContent(owner, replaceOwner) {
composer.runGroup(signature = "content") {
composer.remember(emptyList()) { Any() }
}
}
}
}
}
val firstOwnerState = compose(replaceOwner = false)
owner = "account-B"
revision += 1L
val secondOwnerState = compose(replaceOwner = true)
check(firstOwnerState !== secondOwnerState)
composer.dispose()
完整生成参考位于
viewcompose-runtime API 树。
由于当前版本仍为 Alpha,文档站不会提供稳定的 latest 别名。
状态与生命周期契约
MutableState的等价判断和快照冲突行为由其SnapshotMutationPolicy决定。等价写入不会推进 全局快照,也不会通知读取者。Snapshot在释放前会固定保留历史记录。读取快照不再使用时,必须调用close、dispose或 Kotlinuse。MutableSnapshot应在应用或放弃后释放。冲突失败不会改变目标,可以重试;成功应用是终态。Observation拥有收集期间读取的全部状态订阅。一次成功的全局 Apply 最多使其失效一次, 多个受影响 Observation 按首次观察的稳定顺序交付。不再需要通知时应将其释放,避免状态继续 持有订阅;已经与释放形成竞态并开始的 Callback 可以执行完成。PreparedObservationReplacement是终态对象:外部候选工作成功或失败后,必须且只能调用一次commit或abort。Prepare 会保留已提交订阅,并临时订阅候选独有依赖,避免读取和发布之间 丢失更新或重复 Callback。同一个 Observation 同时只允许一个 Prepared Replacement。- 每个
snapshotFlowCollector 拥有独立的读取观察。取消或计算失败会将其释放;计算必须无 副作用,并且运行次数可能多于发出值的次数。 ComposerLite与派生状态实例按线程封闭设计。宿主负责串行化组合、prepared commit/abort、effect 投递和释放。- Reusable Content Owner 只能在正在执行的 Group 中变化,并且该次 Transfer 必须设置
replaceOwner。Owner 会参与 Remember 与 Saveable Identity,因此必须在一个逻辑生命周期内 保持稳定,不能与物理容器 Identity 混淆。 - Composition Timing Collector 只在一次同步
prepareRootWithTiming调用期间有效。它不得保留 Scope、调用应用代码、阻塞、执行 I/O 或重入 Composer;Collector 失败与组合隔离。普通prepareRoot路径不分配 Timing Identity、不执行逐 Scope 时钟读取,也不保留 Timing History。 - Remembered 生命周期对象会保持 Pending,直到
onRemembered成功返回。激活抛错后,后续 成功的 Composition Commit 会重试它,但不会再次激活已成功的兄弟对象。激活前移除会调用onAbandoned;Active 值则通过恰好一次onForgotten终止。Abort 不会终止已提交对象,也 不会激活候选替换对象。 ComposerLite.composeRoot会提交 Runtime State,但不会执行一次性 Side Effect。宿主只有在 对应渲染树与 Remember 生命周期事务都提交成功后,才调用commitSideEffects。ComposerLite.rememberUpdatedState仅向活跃组合线程暴露候选值,在已提交生命周期回调前 发布该值,并在 Abort 时丢弃它。ComposerLite.scopedExplicitSaveableKey根据当前结构 Key 路径派生显式rememberSaveableRegistry Key。Lazy List、Pager 等子 Session 所有者通过此边界隔离恢复 状态,因此不同逻辑条目中相同的应用 Key 不会共享状态,物理 Holder 变化也不会改变逻辑所有者。 若两个不相等的活跃 Keyed Group 产生相同结构路径 Hash,Runtime 会在注册 Saveable Provider 前失败,而不会共享恢复状态;因此自定义 Saveable Key 必须提供稳定且无碰撞的 Hash。- 显式 Keyed Sibling Group 可以移动,同时完整保留 Scope Identity,包括 Remember Slot、 Observation、Child 与 Saveable Path。同一 Parent 下重复的有效 Key/Signature 会让组合尝试 失败,防止两个逻辑条目共享状态。
- Callback 失败会保留原始 Throwable,并附加有界 Effect Kind、Operation、结构 Scope、Slot
与不持有 Key 对象的 Metadata。Host 可以通过
ComposerLite构造参数选择非负的同步 Callback 警告阈值。
长期持有旧快照会保留额外的值记录,频繁改变结构组顺序会阻止组合复用。这些操作也不会阻止 任意用户计算;调用方应把昂贵工作移出状态访问器与组合块,或显式缓存结果。
当前工作树的契约加固
这些修正属于尚未发布的工作树,不会追溯改变已发布制品的行为。嵌套快照固定创建时父方可见的 待提交值,只有后续目标写入参与冲突判断;只读子快照保留相同基线。成功提交,包括空提交,都会 禁止再次进入、写入或提交;子快照不能向已释放或已提交的父方发布。
派生缓存区分可变视图和本地写入。最后一个消费者释放后,上游订阅随之退出。独立读取不保留订阅 但仍保持结果有效,重新观察会在返回前连接依赖。计算失败保留之前的订阅集合,使之后的失效通知 能够触发再次尝试。
通知发生在发布并进入终态之后。每次提交会尝试通知每个受影响的 Observation,直接与派生路径 合计最多一次,重入提交则独立处理。首个回调异常会被重新抛出,后续异常加入 suppressed。此时 值已经提交,因此在收到该错误后重试整个事务可能重复执行应用工作。
2026-09-06 接受的审查基线 d64710459df73f3b42067767bb2f4f273b9eff33 中,兄弟快照的
派生值为 10/10,预期为 10/20;嵌套提交出现一个虚假冲突;消费者释放后保留 100 个上游观察者;
首个观察者抛错后,正常观察者收到零次通知。首轮候选源码执行分别得到 10/20、嵌套提交成功、
零个残留观察者,以及完整通知和终态保护。四个已复现运行时缺陷降至零,在这四个场景中减少
100%;正确性结论为“改善”。基线 818 项独立 JVM 测试和首轮候选 835 项测试均通过,新增的
17 项覆盖隔离、空值、嵌套合并、生命周期、回调失败和重入。比较使用 Kotlin 2.2.10、JDK 21、
JVM target 11 和相同源码聚合测试工具,不证明延迟改善、Android 行为或 Gradle 依赖隔离。
下一步通过 Gradle 执行最终模块测试与编译示例,再验证文本、动画和手势集成后关闭执行计划。
相关文档
Android 应用通常通过 viewcompose-ui-foundation 或 viewcompose-host-android 传递使用本产物。
只有在自身公共 API 暴露其类型,或者开发自定义宿主/运行时集成时,才需要显式依赖。
兼容性说明
0.1.0-alpha02 首次建立快照和轻量组合契约,没有更早的稳定版本迁移路径。不要把内部快照
标识符、组合 saveable key、诊断结构或实现类名持久化为长期外部数据;只有公共 API 参考明确
描述的行为属于受支持契约。
本版本新增 snapshotFlow,并因此把 Kotlin Coroutines 暴露为 API 依赖;同时移除 Alpha 阶段的
ComposerLite.disposableEffect Slot API。自定义组合集成应把所有权工作迁移到 Remembered
RememberObserver,应用 UI 则使用 viewcompose-ui-foundation 的 Effect API。Prepared
Composition 现在强制执行 Owner Thread、终态释放与 Callback Re-entry 边界。Remember 激活失败
可重试;显式 Keyed Sibling 会作为完整 Scope 移动,重复有效身份则快速失败。
最终运行时验证
最终候选增加了一个终态上下文回归:即使在 enter() 内调用 apply(),全局通知也在已提交的全局
视图中执行。返回后恢复调用方上下文,已提交快照仍禁止继续读写。新增用例先失败,隔离通知上下文后通过。
完整 Gradle 任务 :viewcompose-runtime:test 通过 116 项测试,其中 19 项为加固用例,失败和跳过均为零。
相较接受的四个审查缺陷,四个探针均得到修正,正确性结论仍为改进;扩大覆盖不等于性能测量。
后续保留模块回归,并单独使用真机和高扇出基准评估性能。