跳到主要内容

迁移 Lazy 集合 Revision 与复用

范围

本指南说明从依赖 Callback 的 contentToken 行为,硬切到显式逻辑条目 Revision 与独立物理呈现 复用。适用于 LazyColumnLazyRowLazyVerticalGridHorizontalPagerVerticalPagerTabRow、自定义 LazyListItemSession,以及包含 AndroidView 的 Lazy Item。

用语义 Revision 替换 Content Token

把 Item 与 Page 的 contentToken 参数改为 contentRevision。它不再是宽松提示:Key、Content Revision 和框架 Environment Revision 相等时,条目会完全跳过 Render。变化的普通 Kotlin 捕获值 必须进入 Revision。

LazyColumn(
items = messages,
key = { message -> message.id },
contentType = { "message-row" },
contentRevision = { message -> message.version },
) { message ->
MessageRow(message)
}

批量 Item Overload 只有在不可变值模型的 Equality 覆盖 Item Content 所读取全部普通非 State 值时, 才能保留 { it } 默认值。可变模型需要显式不可变 Version 或 Snapshot。Item Content 在 Active Session 中读取的 ViewCompose State 已经可观察,无需重复放入 Revision。

单条 itemstickyHeader、Pager PageTab Declaration 不再把 Key 默认用作 Content Revision;其 contentRevision 必传且不可空,null 不是静态哨兵。只有 Declaration 没有会变化的 普通非 State 输入时,才能使用 StaticContentRevision

stickyHeader(
key = "messages-header",
contentRevision = StaticContentRevision,
) {
Text("Messages")
}

Pager Page 现在暴露全部调用方快照字段:

Page(
key = account.id,
contentRevision = account.version,
contentType = "account-page",
) {
AccountPage(account)
}

这些单条 Declaration 把 contentRevision 紧跟在 key 之后,再排列 contentType、网格 span 等可选物理复用或布局参数。这样逻辑标识和语义内容 Revision 保持相邻,物理呈现策略位于其后。 批量 items Overload 则有意保留可空的 contentRevision: (T) -> Any? = { it } Selector:可空 元素或 Selector 结果可以是真实的不可变模型状态,而单条 Declaration 必须传入有意的非空 Revision 或 StaticContentRevision

这是 Alpha 阶段的源码破坏性变更。对于位置参数源码,仅重新编译并不足以完成迁移。旧的三位置调用 item(key, contentType, contentRevision)Page(key, contentType, contentRevision) 在签名变化后仍 可能通过类型检查,因为两个语义值都接受 Any;但它会把旧 contentType 当作 Revision,把旧 Revision 当作物理 Content Type。必须改写为 item(key, contentRevision, contentType)Page(key, contentRevision, contentType)。维护中的源码应优先使用语义命名参数:

item(
key = message.id,
contentRevision = message.version,
contentType = "message-row",
) {
MessageRow(message)
}

完成源码审计后,所有使用方都必须重新编译,不能把针对旧单条参数顺序编译的 Binary 与新 Artifact 混用。在 JVM 上,相邻的 Any?/Any 参数可能都擦除为同一个 Object Descriptor,因此旧调用不 一定在链接时失败,反而可能把原 contentTypecontentRevision 绑定到相反语义。命名参数可以 保护已审查的源码调用,但不能让已经编译的旧调用变得安全。

Pager Page 与 Tab 现在都要求显式且唯一的 Key。位置是物理排布,不是逻辑标识,框架不再猜测同一 位置的无 Key Child 拥有旧 Child 的 Remember、Saveable State 或 Effect。

框架自动把主题、Android 资源、Locale、方向、Density、Font Scale 与其他 Active Local 捕获进 environmentRevision,应用无需在 contentRevision 中重复这些值。

用显式 Snapshot 值替换聚合 Token

Typed LazyColumnLazyRowLazyVerticalGrid、Scoped items 及其 ScrollableScope Wrapper 不再接受调用方持有的聚合 Snapshot Revision。使用过中间版本 API 的调用应删除 snapshotRevision

LazyColumn(
items = messages,
key = { message -> message.id },
contentType = { "message-row" },
contentRevision = { message -> message.version },
) { message ->
MessageRow(message)
}

现在每次 Declaration Pass 都会求值 List 顺序与成员,并调用 keycontentTypecontentRevision 和网格 Span Selector。框架不会信任 List 身份、List Equality 或独立维护的 Version 来绕过这些校验,从而避免调用方忘记推进平行 Token 时产生过期顺序、成员或 Selector 结果;Scoped Declaration 也不再需要调用方定义 Token 命名空间。

执行 Selector 不会放弃 Keyed 复用。求值完成后,Key、Content Revision、框架 Environment、 Content Type、Item Kind 与 Span 都相等时,会复用已提交的逻辑 Item 与 Session Binding;变化的 Row 仍会定向刷新。Item Session 内读取的可观察 State 会独立跟踪。ViewCompose 没有能够识别任意 Kotlin Capture 的编译器转换,因此 Item Content 读取的每个变化普通非 State 值仍必须进入受影响 Item 的 contentRevision。针对中间版本聚合参数 Method Descriptor 编译的调用方必须为本次 Alpha 硬切重新编译。

对于顶层或 ScrollableScope 的均质容器,已经持有不可变 List Submission 的应用可以选择强类型 整表 Snapshot 快路:

val lazyMessages = remember(messages) {
messages.toLazyItemsSnapshot()
}

LazyColumn(
items = lazyMessages,
key = { message -> message.id },
contentType = { "message-row" },
contentRevision = { message -> message.version },
) { message ->
MessageRow(message)
}

toLazyItemsSnapshot() 会浅拷贝有序 Item 引用并创建新的不透明 Identity;它不接受或执行 Selector。 每个消费容器第一次在某个框架 Environment 中声明该 Identity 时执行 Selector,并保留当前和上一个 成功提交的 Snapshot/Environment Pair。精确 Pair 会以常量时间恢复有序逻辑 Item List,不执行 Selector 或 Key 扫描。新 Identity 或 Environment 变化会 Cache Miss,并走普通 Keyed Canonicalization 路径。 只有 Item Content 在 Active Session 中执行时读取的 State 会独立观察。Selector 读取的 State 或其他 变化输入要求替换 Snapshot,因为精确命中会跳过 Selector。Selector 失败或 Key 重复不会发布已求值 Snapshot,因此用相同 Identity 与 Environment Retry 时会重新执行全部 Selector。

顺序、成员、保留的 Item 数据、Selector Capture 或普通非 State Item Content Capture 变化时, 必须替换 LazyItemsSnapshot。这些 Item Content Capture 还必须进入受影响的 contentRevision; 框架仍没有能够推断它们的编译器转换。每轮 Composition 都新建 Snapshot 仍然正确,但会失去 Identity 快路。Scoped LazyColumn { items(...) }LazyVerticalGrid { items(...) } 有意不提供 LazyItemsSnapshot Overload,并继续在每轮 Declaration Pass 执行 Selector。

用一个显式根节点包装延迟 Sibling

每个 Lazy itemstickyHeader、Typed Item Content 调用和 Pager Page 都拥有一个原生 Holder, 现在必须只发射一个根节点。旧的多根行为会把 Sibling 静默放进同一个中立 Holder,却没有定义它们 应该纵向、横向还是覆盖布局。本次硬切会在 Composition Prepare 阶段拒绝零个或多个根节点,不会 提交任何原生候选。Entry 有意不显示内容时使用 Spacer

把隐式 Sibling:

item(key = "account", contentRevision = account.version) {
Text(account.name)
Text(account.status)
}

改为显式布局所有者:

item(key = "account", contentRevision = account.version) {
Column {
Text(account.name)
Text(account.status)
}
}

同一规则也适用于 HorizontalPagerScope.PageVerticalPager Page。TabRow 仍是 Eager Parent Content,不受此延迟 Holder 限制。

更新原生互操作复用

包含 AndroidView 的 Lazy Mounted Tree,只有所有互操作节点都声明 onReset 才能跨 Key。 Reset 只做可重放配置清理,一次性发布放在 onCommit,永久资源清理放在 onRelease

AndroidView(
factory = { context -> PlayerView(context) },
update = { view -> bindPlayer(view as PlayerView, item) },
onReset = { view -> resetPlayer(view as PlayerView) },
onRelease = { view -> (view as PlayerView).release() },
)

旧逻辑 Session、Remember State、Subscription 与 Effect 会先结束,Renderer 才能把相同 contentType 的物理树交给另一个 Key。有界 Renderer 缓存会在淘汰时最终 Release;RecyclerView 只池化空 Holder 外壳。无法安全支持该生命周期时不要提供 onReset

更新容器假设

  • Pager offscreenPageLimit-1 时使用 Renderer 的 RecyclerView 缓存策略;只有应用明确 需要两侧各增加对应数量的整页布局空间时,才传至少 1 的值。
  • 删除所有 focusFollowKeyboard 参数。LazyColumn、LazyVerticalGrid 和 ScrollableColumn 中的 焦点编辑器现在自动使用 Android 原生矩形请求链。VerticalPager 页面可能被 IME 遮挡时,必须 把表单放进页内 ScrollableColumn、LazyColumn 或其他真实垂直滚动所有者;Pager 只负责页面选择。
  • TabRow 是 Eager Keyed Parent Content,不再拥有 Lazy Child Session。稳定 Tab Key 在重排时保留 Remember/Saveable Identity,选择变化只失效旧选中项与新选中项。
  • CollectionReusePolicy.mountedTreeCacheSize 限制每个集合保留的 Reset 物理树;0 会关闭 Mounted Tree 缓存,但不改变逻辑正确性。
  • LazyLayoutPrefetchPolicy.nestedInitialPrefetchItemCount 替换 initialPrefetchItemCount。未知或昂贵 Type 不做同步原生准备。

更新自定义 Session 与 Renderer

直接 NodeSpec Producer 还必须把 Alpha 集合边界从 List<LazyListItem> 迁移到 LazyItemTable

LazyColumnNodeProps(
contentPadding = LazyContentPadding.None,
spacing = UiDp.Zero,
items = itemModels.asLazyItemTable(),
)

Foundation LazyColumnLazyRowLazyVerticalGrid DSL 调用方式不变。有限 Adapter 会校验 Key 唯一性并保留普通 Keyed Diff 行为。自定义紧凑 Source 可以直接实现 LazyItemTable,但其 Snapshot 必须不可变;getindexOfKey 必须同步且无副作用;每个声明的 LazyItemTableUpdate 必须精确变换已识别的前驱。返回 null 会请求有限通用 Diff,返回 ReloadAll 则表示显式保守替换。无效操作会拒绝完整候选,不会局部更新 RecyclerView。

自定义 Renderer 不得枚举紧凑 Table 来预建全部 Key 或 Stable ID。应通过 indexOfKey 解析位置, 分配不依赖应用 Hash 的无冲突物理 ID,并消费可选 LazyItemTableStickyHeaders 元数据。不提供该 元数据的 Table 承诺不含 Sticky Entry。遍历 Table 属于有限兼容扫描,对虚拟位置可能成本极高。

自定义 LazyListItemSession 必须保持完整生命周期:可选且不对外发布的 prepare;首次呈现前一次 activate;只在内容或环境 Revision 改变时 render;通过 disposeForReuse 结束全部逻辑 Owner, 再返回 Reset 物理呈现;最终 disposeReusableItemPresentation.release 必须幂等。

activaterender 现在只有在已安装内容真正 Commit 时才返回 true。Rollback 后应返回 false,这样 Renderer 不会推进 Item Revision,并可重试同一 Submission。原生帧一旦 Commit, 后续 Side Effect 或诊断失败不会改变返回值。

LazyListItem 现在持有一个 sessionStrategy 与一个不透明 sessionPayload;原来的 sessionFactorysessionUpdater 构造字段已移除。Strategy 会在 createupdate 中同步接收 当前 Item,读取 Payload,但不得保留 Item Snapshot。create 安装初始 Payload,update 把变化的 Payload 安装到现有 Session。Key 与 Type 相同时,Revision 变化绝不允许用替换逻辑 Session 作为 实现兜底。

Typed 与强 Snapshot Declaration 会让全部 Item 共享同一个 Strategy,因此已提交存储不再为每一行 保留 Factory/Updater Wrapper 和捕获 Item 的 Content Closure。不需要读取 Payload 的底层静态实现 可以使用 lazyListItemSessionStrategy(create, update);需要 Payload 的实现应直接实现 LazyListItemSessionStrategy

Adopt 返回 false,或在所有权转移前抛出异常时,呈现会立即 Release。第一次跨 Owner Rebind 失败时不能调用旧逻辑 Owner 的 Update,也不能恢复其可见帧;被 Adopt 的树必须释放。

验证

运行仓库单测与文档门禁,再使用 Release 构建检查 Diagnostics Route。在 Theme、Renderer 与 Gaps 之间切换后,立即执行到底部并返回顶部的大力度长 Fling。确认相等 Revision 不 Render,变化 Revision 只更新目标 Key,旧 Effect 在原生 Reset 前释放,缓存淘汰只 Release 一次。

当前架构参见 ADR-0012Lazy 集合指南