跳到主要内容

迁移 Compose 布局、Modifier 与环境代码

本文对比 Jetpack Compose 与 ViewCompose 的布局、Modifier 和 CompositionLocal 语义。它是一份 工程迁移参考,而不是 API 名称对等表。语法相似并不表示测量、生命周期、失效或 Android 集成 行为等价。

基线、状态术语与验证日期

基线版本用途
ViewCompose 目标模块runtime 0.1.0-alpha02;UI Contract 与 Host 0.1.0-alpha03;UI Foundation 与 Renderer 0.1.0-alpha01本迁移指南的目标版本
Compose Runtime、UI 与 Foundation1.11.4 stable上游语义参考
仓库 Compose 依赖1.7.8本仓库中的可执行对照基线
仓库 Kotlin 工具链2.0.21对照代码的编译基线

上游基线由 AndroidX 官方的 Compose RuntimeCompose UICompose Foundation 发布说明确认。仓库基线声明在固定 revision 的 gradle/libs.versions.toml 第 3 行和第 22 行。

本文严格使用四种能力状态:

  • Supported:迁移目标保护相关可观察行为,尽管名称或实现细节可能不同。
  • Partially supported:存在可行替代方案,但 Compose 契约的重要部分缺失或范围更窄。
  • Intentionally different:ViewCompose 提供刻意设计的替代契约;代码需要重新设计,而不是简单改名。
  • Unsupported:在已验证基线中不存在公开等价能力。

最后验证日期:2026-08-06

复核负责人:ViewCompose UI Contract、UI Foundation 与 Android Renderer 维护者

证据模型

本对比包含两层不能混为一谈的证据:

  1. 官方语义复核使用 Android Developers API 文档、行为指南和 Compose 1.11.4 的 AndroidX 发布说明。这些来源定义本文描述的上游行为。
  2. 本地可执行证据使用上面这组独立版本化的 ViewCompose 目标源码契约和仓库测试。仓库的 Compose 1.7.8 依赖可用于编译对照,但不能用来否定 Compose 1.11.4 已记录的语义变化。

本文不声明性能等价。本次复核没有为 Compose 布局节点与 Android View 建立可比较的基准测试条件。

可编译的成对起点

下面的对照在两侧都保留一个水平布局、一条有序 Modifier 链和一个作用域环境值。代码片段从 已编译的 :samples:compose-migration 模块提取,并由 qaQuick 检查是否与源码完全一致。

Compose 源码:

private val LocalContentPadding = compositionLocalOf { 8.dp }

@Composable
fun ComposeProfileRow(name: String) {
CompositionLocalProvider(LocalContentPadding provides 16.dp) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.padding(LocalContentPadding.current),
) {
BasicText(name)
}
}
}

ViewCompose 目标:

private val LocalContentPadding = uiLocalOf { 8.dp }

fun UiTreeBuilder.ViewComposeProfileRow(name: String) {
ProvideLocal(LocalContentPadding, 16.dp) {
Row(
verticalAlignment = VerticalAlignment.Center,
modifier = Modifier
.fillMaxWidth()
.padding(UiLocals.current(LocalContentPadding)),
) {
Text(name)
}
}
}

相似的代码结构不表示引擎等价。Compose 测量布局节点并跟踪 CompositionLocal 读取; ViewCompose 渲染 Android View、按 renderer 规则折叠 Modifier 元素,并把 UiLocal 当作作用域 查询,而不是失效订阅。

能力矩阵

概念Compose 1.11.4 行为ViewCompose 已验证版本集合行为状态必需的迁移动作
内置布局容器RowColumnBox 和 Foundation 布局在 Constraints 下测量 Compose 布局节点。RowColumnBox、流式布局、滚动容器和 ConstraintLayout 发出 VNode,并最终成为 Android ViewGroup 实现。Partially supported在原生 View 实现上重新检查默认值、溢出、裁剪、weight 和固有尺寸假设。
自定义测量LayoutMeasurePolicy 和布局 Modifier 节点允许应用代码测量并放置 Compose 子项。普通测量中每个子项只能测量一次。未发现公开的通用测量策略、measurable/placeable 契约或布局 Modifier。自定义多子项测量需要 renderer 扩展,或通过互操作托管 Android ViewGroupUnsupported围绕内置容器或具有生命周期所有权的 Android View 实现重新设计自定义 Compose 布局。
尺寸与填充布局 Modifier 会变换或约束 Modifier 链;size 仍受传入约束限制,支持相关重载时 fill API 可接受比例。精确 dp 尺寸会成为像素 LayoutParams;fill 辅助方法会成为 MATCH_PARENTmaxWidthmaxHeightaspectRatio 围绕完整节点共享一个 Renderer 测量边界。Partially supported迁移最终测量契约,而不只是函数名。审查 Exact/Min/Max 冲突、比例轴优先级、比例填充、required size 和固有尺寸行为。
Padding 与 margin每个布局 Modifier 都在 Modifier 链中的当前位置参与处理。Compose 通常通过布局结构或 padding 表达外部空间,而不是 margin 属性。Padding 是原生 View 内容内边距。Margin 是显式的原生父级 LayoutParams 数据。重复 padding 或 margin 元素会解析为该类型的最后一个元素。Intentionally different将重复 padding 规范化为单个预期值,并明确判断原外层 padding 应属于父级结构、View padding 还是 ViewCompose margin。
作用域父数据RowScope.weightColumnScope.weight、对齐和 BoxScope.matchParentSize 等作用域安全 Modifier 向兼容的直接父级提供数据。RowScopeColumnScope 提供 weight 与交叉轴对齐;BoxScope 提供对齐。错误使用父数据会产生警告。没有已验证的 matchParentSize 等价能力。Partially supported仅在匹配容器的直接子项上使用作用域 Modifier。重新设计 matchParentSize;不要直接替换为 fillMaxSize
Constraint 父数据Compose ConstraintLayout 在自己的测量模型中消费 layout ID 和 constraint 父数据。可选 ConstraintLayout 模块把 layout ID 和 constraint spec 映射为 AndroidX ConstraintLayout LayoutParams 与 ConstraintSet 操作。Partially supported针对 AndroidX View 实现重新验证尺寸、baseline、RTL 锚点和依赖环。
布局坐标动画Compose 基于 Lookahead 的 Bounds Motion 参与 Compose 布局坐标,并可协调更广泛的视觉转场。Modifier.animateBounds 在直接 ViewCompose Parent 的物理像素中动画一个真实的位置与尺寸矩形。该 API 不包含 Reparent、Shared Element 或跨 Owner 视觉连续性。Partially supported保持节点位于一个稳定 Parent 下,迁移最终尺寸、对齐或 Constraint 状态,拒绝同时使用 animateContentSize,并在 Ownership 变化时使用普通目的地 Enter 行为。
Modifier 顺序Modifier 元素形成有序包装链;顺序可以改变测量、绘制、输入、焦点和语义。源码链有序,但 renderer 会将其折叠为分阶段值。许多重复值采用后者覆盖前者,部分冲突使用固定优先级,z-index 会相加,draw 或 shadow 分组保留顺序。Intentionally different迁移前,按照 ViewCompose 解析规则为每条非平凡 Modifier 链分类。
Modifier 相等性与更新ModifierNodeElement 通过相等性决定是否更新已有 Modifier.NodeModifier 链按有序元素序列进行结构比较。相等的链可使 renderer diff 跳过子树。NativeViewElement 的相等性只使用稳定 key,忽略回调身份。Supported当原生配置的语义发生变化时使用会变化的 key,不要依赖新的 lambda 实例强制更新。
自定义 Modifier.Node 生命周期公开节点 API 提供 create/update、attach/detach、失效、local 读取,以及专门的布局、绘制、输入或语义节点接口。ModifierElement 是 renderer 标记,不是应用生命周期节点。内置元素由已知 renderer 分支解释。不存在自定义节点 attach/detach 或能力接口的公开等价物。Unsupported使用受支持 Modifier、可重放的 nativeView、具备事务语义的 AndroidView,或经过评审的 renderer 功能。不要从应用代码发布 renderer 无法识别的元素。
Density 与字体缩放LocalDensity 为布局和绘制代码提供 dp/sp 转换。UiDensity 会被捕获进每个 VNode 环境。Android host 从资源读取 density 和 font scale;renderer 在原生边界转换单位。Supported在声明中保留逻辑 dp/sp,不要跨新的环境快照保留已转换像素。
布局方向与 localeCompositionLocal 提供布局方向和 locale 数据;逻辑 start/end API 根据该环境解析。方向和 locale 列表会被捕获到 VNode。Renderer 应用原生 View 方向和 TextView locale。通用边缘 API 同时提供显式物理与相对形式,延迟 Session 会携带环境 Revision。Supported逻辑 start/end 意图使用相对形式;只有明确的 left/right 行为才保留物理 API,并执行真实 RTL 布局检查。
CompositionLocal 传播compositionLocalOf 跟踪读取点;提供值发生变化时使读取者失效。staticCompositionLocalOf 以更大粒度使 provider 内容失效。UiLocal 在构建树时使用线程作用域 map。Emit 会把完整 local 快照作为输入比较,但读取 UiLocals.current 本身不会登记失效依赖。Intentionally different用 ViewCompose state 或其他 host 失效来源承载会变化的 local 值。把 local 读取视为作用域值查询,而不是观察。
延迟内容 localLazy 和其他 subcompose 内容通过拥有它的 Compose composition 观察 local。Lazy、pager、tab、overlay 和 navigation session 会显式捕获不透明 local 快照,并在延迟内容渲染时恢复。快照变化会参与 content token 或 session 更新。Supported保持稳定的 item/page key 与 content token,让容器刷新捕获的快照,而不是保留 builder。
系统栏与 IME insetInset padding 感知布局、参与自动嵌套消费、避免重复应用已消费部分,并跟随 IME 更新和动画。系统栏与 IME Modifier 在目标 View 上安装 AndroidX listener,并把选中的物理边或按方向解析后的边加到基础 padding。嵌套 ViewCompose Modifier 不交换已消费 inset 状态;同一 View 上的系统栏与 IME 值会相加。Partially supported为 inset 指定明确的所有者层级,避免祖先和后代重复应用,并避免把 adjustResize 与重复的 IME padding 组合。
Android 输出与 View 互操作Compose 通常渲染 Compose 节点;AndroidView 嵌入平台 View,并提供 factory/update 以及可选的复用/释放回调。所有第一方节点最终都成为 Android View。ViewCompose AndroidView 增加事务回滚和事务后提交语义;nativeView 对已挂载 View 应用可重放配置。Intentionally different将可重复配置、一次性工作和清理分开,分别放入 update/native 配置、onCommitonRelease

两种布局引擎:Compose Constraints 与 Android Views

Compose 布局是一套节点协议。父级传递约束,子项报告测量尺寸,父级再放置得到的 placeable。 官方自定义布局文档还定义了普通 布局的单次测量规则和公开的 Layout 逃生通道。

ViewCompose 会先构建不可变 VNode。Android renderer 随后创建原生 widget 和容器。例如,Text 会成为 TextView,Row 和 Column 会成为设置了方向的 DeclarativeLinearLayout,Box 会成为 DeclarativeBoxLayout。完整映射见固定 revision 的 ViewNodeFactory.kt 第 55–126 行。Row 和 Column 保留 Android LinearLayout 测量,并在原生放置阶段实现声明式 arrangement;参见 DeclarativeLinearLayout.kt 第 21–92 行。

因此,Compose 自定义 Layout 无法翻译成普通 ViewCompose 组件。请选择以下边界之一:

  • 使用内置 ViewCompose 容器表达结果;
  • 当 constraint 父数据足够时使用可选 ConstraintLayout 模块;
  • 当应用专属测量不可或缺时,通过 AndroidView 托管自定义 Android ViewGroup;或
  • 当该行为属于可复用框架契约时,提出有文档支撑的 renderer 功能。

后两种选择并不等同于接收 Compose Measurable。Android measure spec、LayoutParams、 request-layout 传播和平台 View 状态仍是权威行为。

Size、padding、margin 与 fill 语义

Compose 会让布局 Modifier 作为有序参与者在约束传播中依次处理。上游模型以官方 约束与 Modifier 顺序指南 为准。

ViewCompose 通过原生 LayoutParams 解析尺寸。感知父级的优先级如下:

  1. ConstraintLayout dimension(如果存在);
  2. 轴向专用的 widthheight Modifier;
  3. size 对应轴的值;
  4. renderer 根据节点和父级确定的默认值。

该优先级实现在固定 revision 的 ViewLayoutParamsFactory.kt 第 73–99 行。精确 dp 尺寸使用 VNode 捕获的 density 转换。Fill 辅助方法映射为 Android MATCH_PARENT;它们不会保留 Compose 的所有比例或固有尺寸选项。

可移植的最大边界和比例是直接 LayoutParams 映射的例外。Renderer 会把 maxWidthmaxHeightaspectRatio 折叠为包裹完整节点的一个合成测量 Host。它们必须是正有限值。Exact 或 Minimum 尺寸超过声明 Maximum 时会确定性失败,而不是静默选择一个来源;比例默认优先使用宽度,除非设置 matchHeightConstraintsFirst。当不存在同时满足双轴精确约束与比例的尺寸时,Android 传入的 EXACTLY 约束保持权威。这不是公开自定义测量 API,也不会让任意 Compose LayoutModifier 代码变得可移植。

边缘 Modifier 有不同的原生落点:

  • padding 成为物理内容内边距;paddingRelative 会在 Renderer 写入已挂载 View 前映射逻辑 start/end;
  • margin 提供物理 LayoutParams Margin;marginRelative 会在创建父级 LayoutParams 前映射 逻辑 start/end;
  • offset 是物理 View Translation;正 offsetRelative.horizontal 朝逻辑 end 移动,且两种 Offset 都不改变兄弟节点测量或放置;
  • minimum width 和 height 成为 View 最小尺寸。

重复 Padding 不会创建嵌套布局层。物理与相对声明在每一族中共享一个解析槽,因此后声明的 Padding、Margin 或 Offset 会整体替换先声明值,即使二者形式不同。迁移时应先规范化 Compose Modifier 链,并通过容器结构保留预期的内外边界。

公开尺寸与边缘契约见固定 revision 的 ModifierLayoutExtensions.kt 第 6–187 行和第 189–290 行。原生 LayoutParams 应用见 ViewLayoutParamsFactory.kt 第 91–149 行和第 168–192 行。

Row、Column、Box 与作用域父数据

两个框架都使用 receiver scope,使常见父数据靠近兼容父级。Compose 行为和 matchParentSize 区别记录在 Compose modifiers 中。

ViewCompose 提供以下受支持作用域操作:

Scope支持的父数据原生落点
RowScope正数 weight;垂直 align横向 LinearLayout weight 与子项 gravity
ColumnScope正数 weight;水平 align纵向 LinearLayout weight 与子项 gravity
BoxScopeBox alignFrameLayout 子项 gravity

声明和正数 weight 校验见固定 revision 的 LayoutScopes.kt 第 12–96 行。父数据校验见 ModifierParentDataValidator.kt 第 28–97 行。

这些 receiver scope 暴露的操作是受支持的应用 API,但作用域本身并不是完整的运行时类型安全 屏障。renderer 集成仍可看到契约元素类,不兼容父级只会产生去重后的警告,而不会导致渲染 失败。应把每个作用域 Modifier 都视为直接子项数据。

本文刻意把 Compose BoxScope.matchParentSize 标为 Unsupported。Compose 使用它匹配 Box 的最终尺寸,但不会让该子项决定 Box 尺寸。ViewCompose fillMaxSize 映射为 MATCH_PARENT, 不得把它记录为等价替代方案。

ConstraintLayout 父数据

可选 ConstraintLayout 模块以父数据形式提供 layout ID 和 constraint item spec。Android renderer 通过 AndroidX ConstraintLayout 消费这些值。固定尺寸从子项捕获的环境转换; MatchConstraints 使用 Android ConstraintLayout 的零尺寸约定。

两个库都使用专用且带 Marker 的 Content Scope,但 ViewCompose 有意保留 XML 用户熟悉的 startToStarttopToBottom 等函数,而不是照搬 Compose Anchor Object。ViewCompose 会区分 水平、垂直与 Baseline Target 能力:把 Top/Bottom Guideline 用作 Start/End Target,或把 Start/End Guideline 用作 Top/Bottom Target,都会在 Kotlin 编译阶段失败。嵌套结构型 DSL 会 隐藏外层 ConstraintLayout Receiver;Helper Metadata 在 Content 完成后冻结,不再通过环境式 Thread-local State 收集。

逻辑 Start/End 语义在挂载后仍由环境驱动。Android Renderer 会保持 Retained Helper 的 layoutDirection 与 ConstraintLayout Container 同步,因此原地 LTR/RTL 切换会镜像逻辑 Guideline 与 Barrier,同时不替换其稳定 Identity。

Reusable Set 同样维持类型化声明 Identity。先创建 Reference,再把它传给 constrain(ref), 并用同一个 Reference 建立 Link;已移除的 constrain(ref.id) 形式不会再漂移到无关 String。 Modifier.constrain(id, ...) 继续作为显式 Inline XML 迁移捷径。Dimension 应迁移到互斥的 WrapContentConstrainedWrapContentFixedMatchConstraints 代数,而不是独立 min/max/percent 字段或 MatchParent

发版后能力线新增类型化 Chain Endpoint 与 Margin、四种 Parent-wrap Contribution Mode、逻辑与 物理 Horizontal Anchor/Guideline/Barrier、支持 Weight/Span/Skip 的类型化 Grid,以及会编译为普通 Circle Constraint 的声明式 CircularFlow。它有意不暴露 AndroidX Grid String Grammar、进程级 CircularFlow Default、命令式 Helper Mutation、原始优化 Bitmask、Compose linkTo 或匿名 Reference。 这些省略用于维持单一类型化 Graph Owner 与 XML-friendly 迁移函数族,不是尚未补齐的 Alias。

契约元素定义在固定 revision 的 ModifierElementsLayout.kt 第 117–150 行。感知父级的转换实现在 ViewLayoutParamsFactory.kt 第 91–98 行和第 247–255 行。

这是可行迁移路径,不是 Compose ConstraintLayout 对等性的证明。应根据 ViewCompose 模块契约 重新检查 ConstraintSet 合并、Baseline 连接、逻辑 Start/End Anchor、循环依赖、Helper 能力和 Dimension 默认值。已完成的 Revision 6 Released/Candidate/Direct 矩阵对发版安全给出 no material change,而不是性能领先结论:12 个 Candidate Action 的 P95 全部由 Direct AndroidX 更快,另有 5 个 Longitudinal Action 保持 inconclusive。选择该模块的原因应是类型化 声明契约、原生 Solver 行为与事务安全,而不是期望迁移后每一帧都更快。详见 ViewCompose 性能

Modifier 顺序、折叠与相等性

两种 Modifier 都是不可变有序链。ViewCompose 追加元素时不会修改 receiver,并对得到的序列 执行结构比较;参见固定 revision 的 Modifier.kt 第 3–56 行。

关键区别在执行方式。Compose 布局和行为节点会保留其在包装节点链中的位置。ViewCompose 会把 元素折叠成 ResolvedModifiers 快照,再由不同 renderer 阶段消费。已验证的折叠规则包括:

Modifier 关系ViewCompose 规则
重复的标量或单槽位元素同类型中靠后的元素通常替换靠前的值。
shape 与旧版 cornerRadius两者互斥;链中靠后的一个会清除靠前的一个。
重复 zIndex数值相加。
Draw 与高级 shadow 分组分组保留声明顺序。
轴向 width/heightsize轴向专用值通过固定 LayoutParams 优先级胜出,与跨类型链顺序无关。
graphicsLayer 与简单 alpha、offset 或 clip提供 graphics-layer 值时,它具有固定 renderer 优先级。
物理与相对 Padding、Margin 或 Offset同一族中后声明的值整体替换先声明值。
系统栏与 IME Inset Padding每个 Inset 类型内由后声明的物理或相对值获胜,随后系统栏与 IME 贡献相加。

折叠逻辑实现在固定 revision 的 ResolvedModifiers.kt 第 72–172 行。不要根据一个 Modifier 家族推断另一个家族的规则。

相等性还会驱动复用。当节点、环境、spec、children 和 Modifier 输入保持等价时, NodeBindingDiffer 可以跳过子树。环境或 Modifier 变化会导致 rebind;参见固定 revision 的 NodeBindingDiffer.kt 第 22–75 行。

NativeViewElement 是特殊情况。它的相等性和 hash code 只使用 stableKey,刻意忽略回调 身份。该契约位于固定 revision 的 ModifierElementsInteraction.kt 第 220–249 行。使用相同 key 的新 lambda 不是更新信号。原生操作语义发生变化时应更改 key, 或让另一个可观察节点输入使绑定失效。

为什么 Modifier.Node 不能直接迁移

Compose 推荐用 Modifier.Node 实现自定义 Modifier 行为。其公开模型包括不可变 element、 保留的 node、create/update、attach/detach、自动或显式失效、CompositionLocal 访问,以及专门的 节点接口。上游参考为创建自定义 ModifierModifier.Node API

ViewCompose 没有等价的公开生命周期节点协议。ModifierElement 是 renderer 所理解契约的标记; 参见固定 revision 的 Modifier.kt 第 59–65 行。应用自定义实现不会被发现为自定义行为。

请根据所有权选择以下替代方案:

  • 框架定义行为使用受支持的 ViewCompose Modifier;
  • 已挂载 View 的可重复配置使用 nativeView
  • 应用代码拥有原生 View 及其释放生命周期时使用 AndroidView
  • 新的可复用 Modifier 能力应通过有文档支撑的 UI-contract 与 renderer 变更实现。

无法识别的 ModifierElement 不是安全扩展点。nativeView 也不是通用节点生命周期:它没有 attach/detach 回调,而且配置可能在回滚时重放。

Density、locale 与布局方向

Compose 通过平台 CompositionLocal 暴露 density 与逻辑方向。官方 Compose 平台 local 参考 定义了 LocalDensityLocalLayoutDirection 和 locale 相关 local。

ViewCompose 会在每个发出的 VNode 上捕获不可变 UiEnvironmentValues,其中包含:

  • UiDensity,包括 density 和 font scale;
  • 有序 UiLocaleList
  • UiLayoutDirection
  • Host 所有的 resourceRevision,用于在 Configuration 或主动资源变化后重新绑定相等的 Android 资源 ID。

快照契约要求平台 Configuration 变化后生成新树。标准 Android Host 会通过资源环境自动调度新树; 自定义 Host 必须显式发布新环境。参见固定 revision 的 UiEnvironmentValues.kt 第 92–112 行。Android bridge 在 AndroidEnvironmentBridge.kt 第 15–29 行读取资源和 configuration。单位转换定义在 UiUnits.kt 第 157–223 行。

绑定时,renderer 把环境存到 View、应用原生布局方向并设置 TextView locale。该边界位于固定 revision 的 ViewModifierApplier.kt 第 41–55 行。环境变化会强制完整节点 rebind,而不是只执行视觉 patch。

Modifier 边界显式区分方向语义。现有 paddingmarginoffsetsystemBarsInsetsPaddingimeInsetsPadding 保持物理语义;对应的 Relative API 会在每次 Bind 和环境重绑时根据捕获的 UiLayoutDirection 解析逻辑 start/end。相对水平 Offset 正值朝 end 移动:LTR 向右、RTL 向左。Top、Bottom 与垂直 Offset 保持物理语义。

迁移非对称水平空间仍必须显式选择。Compose start/end 意图应映射到相对 API;只有产品需求确实 表示物理 left/right 时才使用原 API。不要在应用代码中预先交换数值,因为运行时方向变化以及 延迟 Lazy/Pager Session 都是框架所有的失效输入。

UiLocal 与 CompositionLocal

Compose 区分会追踪读取的 compositionLocalOf 与宽粒度的 staticCompositionLocalOf 失效。 上游行为记录在使用 CompositionLocal 在本地确定数据作用域 中。

ViewCompose UiLocal 是一个类型化句柄,指向构建 VNode 树时使用的线程作用域不可变 map。 ProvideLocal 为嵌套 block 安装值,block 结束后恢复之前的 map。ProvideLocals 为多个绑定 执行相同操作。Binding 是否存在与值是否可空相互独立:可空 Local 显式提供的 null 会覆盖 非空默认值,并在 Capture、Restore 与延迟 Child Session 传播中保持不变。实现位于固定 revision 的 UiLocals.kt 第 3–103 行,以及 LocalValue.kt 第 35–120 行。

关键迁移规则是:UiLocals.current(local) 是查询,不是观察。它不会把调用点登记为依赖读取者。 相反,UiTreeBuilder.emit 会把完整当前 local 快照作为一个 composition 输入捕获。当其他失效 已经触发 composition 时,如果该快照不同,节点 group 会重建。参见固定 revision 的 UiTreeBuilder.kt 第 66–124 行和第 192–214 行。

因此:

  • 把会变化的源数据存入 ViewCompose state,而不是只放入普通 provided object;
  • 不要期望修改相等 local 值内部的可变字段会调度渲染;
  • 优先使用具有有效相等性的不可变 local 值;
  • local 快照变化可能比 Compose 受跟踪 local 读取使更多工作失效;
  • 自定义 host 必须在所属 renderer 线程上串行构建树。

延迟内容与 local 快照

Lazy collection、pager、tab、overlay 和 navigation 可以在声明作用域返回后渲染内容。 ViewCompose 会为这些边界显式保存 local。

对于 lazy list,LazyItemCollector 会捕获 LocalSnapshot、把它包含进有效 content token、 使用该快照创建子 session,并在更新时同时刷新快照和内容闭包。参见固定 revision 的 LazyCollectionScope.kt 第 147–193 行,以及 WidgetLazyListItemSession.kt 第 8–72 行。

这会在 holder 复用期间保留嵌套 local 值,但不会移除调用方的身份责任。Key 必须稳定且唯一。 当 local 快照之外捕获的业务值发生变化时,content token 必须变化。内容 block 返回后,不要保留 或调用 UiTreeBuilder

系统栏与 IME inset

Compose inset padding 会在布局期间使用当前 inset 值,并向嵌套 Modifier 传达已消费部分。 官方 inset UI 指南解释了嵌套 消费、尺寸 Modifier 与 IME 动画行为。

ViewCompose 为每种受支持的 Inset 类型提供物理与相对形式:

  • systemBarsInsetsPadding,用于选择物理系统栏边;
  • systemBarsInsetsPaddingRelative,用于选择逻辑 start/end 系统栏边;
  • imeInsetsPadding,默认选择物理 bottom 边;
  • imeInsetsPaddingRelative,同样默认选择 bottom,也可选择逻辑 start/end 边。

Renderer 会安装 AndroidX WindowInsetsCompat Listener、记录基础 Padding,并加上选中的 Inset 像素。方向变化会根据节点环境重新解析相对选择器;Root Insets 可用时立即使用,否则先清除旧 物理边贡献,等待平台分发替代值。移除两个 Modifier 后会恢复基础 Padding 并移除 Listener。 实现位于固定 revision 的 ModifierInsetsApplier.kt 第 11–128 行。

与 Compose 不同,该 listener 原样返回传入 inset,不会传达祖先 ViewCompose Modifier 已应用的 数量。因此嵌套 ViewCompose inset-padding Modifier 可能再次添加同一个 inset。同一 View 上选择的 系统栏和 IME padding 也会相加,而不是由共享消费模型抵扣。

迁移规则:

  1. 尽可能为每个 inset 边选择唯一所有者。
  2. 检查原生祖先和嵌入 View 是否有自己的 inset 处理。
  3. 除非刻意验证最终位移,否则不要把 Activity adjustResize 与重复的 imeInsetsPadding 组合。
  4. 在真实托管页面上测试手势导航、三键导航、横屏、RTL、display cutout 和一次 IME 过渡。
  5. 不要声明 Compose 嵌套消费或同帧布局对等性。

单元测试保护默认值、物理/相对优先级、方向重解析与兼容 WindowInsets 分发。设备级动画、混合树 消费和平台版本分发行为仍是下文记录的认证边界。

Android View 输出与互操作

Compose 通常渲染自己的 UI 节点,并把 AndroidView 作为互操作边界。官方 在 Compose 中使用 View 指南 定义了 factory、update、reuse、reset 和 release 行为。

ViewCompose 在根本上不同:每个第一方 VNode 都会成为 Android View。其 AndroidView API 仍是应用创建 View 的独立所有权边界,并具有事务感知生命周期:

回调ViewCompose 契约
factory仅当 reconciliation 需要新的原生节点时运行。
update在插入、patch 或回滚期间执行可重复配置。
onReset保留的 View 重新绑定前执行可选的可重复 reset。
onCommit仅在完整 View 树事务提交后发布一次性工作。
onRelease每当已创建 View 被永久放弃时执行一次性清理,包括已提交移除、session disposal 或未提交候选项回滚。

公开契约位于固定 revision 的 AndroidInteropDsl.kt 第 11–82 行。挂载和 commit 调度位于 ViewTreePatchPipeline.kt 第 527–579 行。

updateonResetnativeView 不得启动不可重复的外部工作。失败帧可能恢复之前已提交的 原生树并重放配置。只在成功后运行的操作应放入 onCommit,所拥有资源的清理应放入 onRelease。公共契约与 Renderer 测试都把未提交候选项的回滚视为永久放弃。

迁移检查清单

  1. 记录来源 Compose 版本和目标 ViewCompose 模块的精确版本。
  2. 将每个布局分类为内置布局、基于 constraint 的布局或自定义测量布局。
  3. 先替换布局行为,再翻译视觉 Modifier 名称。
  4. 根据 ViewCompose 折叠规则规范化重复 size、padding、margin、graphics-layer 和 draw 元素。
  5. maxWidthmaxHeightaspectRatio 替换兼容的最大尺寸和比例链,并测试 Exact/Minimum 冲突及有界/无界父容器。
  6. 仅在匹配 scope 的直接子项上使用父数据 Modifier,并重新设计 matchParentSize 用法。
  7. 把逻辑 start/end 意图映射到相对 Modifier;仅为明确的 left/right 行为保留物理 API。
  8. 把会变化的 provided 值移到 ViewCompose state 后面;不要依赖 UiLocal 读取追踪。
  9. 跨 View 与 ViewCompose 边界显式分配系统栏和 IME inset 所有权。
  10. 分离 Android View 可重放配置、提交后工作和释放清理。
  11. 声明迁移完成前,为测量、RTL、local 更新、延迟 session、inset 分发和互操作回滚添加行为测试。

源码与可执行证据

以下本地证据保护本文中的声明:

  • Modifier 不可变性、结构相等性、声明顺序、Draw 顺序、父数据构造与五个公开相对 Modifier 契约:固定 revision 的 ModifierContractTest.kt, 其 Q3 已编译用法位于 UiContractNodeSamples.kt
  • 最大尺寸与宽高比契约验证及编译用法: NativeWidgetContractValidationTest.ktlayoutConstraintModifierSample;Android 测量由 Renderer 模块的 LayoutConstraintHostTestLayoutConstraintNodeWrapperTest 覆盖。
  • Modifier 折叠、z-index 相加、有序 shadow 与 ConstraintLayout 父数据:固定 revision 的 ResolvedModifiersTest.kt, 包括物理与相对边缘形式之间的后声明者覆盖规则。
  • 兼容与不兼容的作用域父数据:固定 revision 的 ModifierParentDataValidatorTest.kt, 第 31–159 行。
  • 结构 Modifier 相等性与环境驱动的 renderer rebind:固定 revision 的 NodeBindingDifferTest.kt, 第 115–141 行。
  • Density、locale、direction、嵌套环境值及其向 VNode 的捕获:固定 revision 的 EnvironmentTest.kt, 第 15–68 行。
  • 感知 density 的 ConstraintLayout 解析:固定 revision 的 DeclarativeConstraintLayoutEnvironmentTest.kt, 第 21–79 行。
  • 嵌套 UiLocal 提供、恢复和显式快照恢复:固定 revision 的 BusinessLocalApiTest.kt, 第 13–103 行。
  • Local 快照稳定性与环境驱动的子树替换:固定 revision 的 SubtreeRecompositionTest.kt, 第 59–123 行。
  • 随捕获 Local 变化的延迟 Lazy、Pager 与 Tab Content Token:固定 revision 的 LazyContentLocalPropagationTest.kt, 包括延迟 Lazy 与 Pager Session 随方向变化更新环境 Revision。
  • 运行时方向变化、物理 Padding/Margin/Offset 结果与 Inset 选择器重绑:固定 revision 的 ViewTreeRenderTransactionTest.kt
  • 原生 ConstraintLayout 兼容 LayoutParams 下的相对 Margin:固定 revision 的 ViewLayoutParamsFactoryRelativeTest.kt
  • Insets Modifier 默认值与共存:固定 revision 的 InsetsPaddingModifierTest.kt, 上述事务测试覆盖兼容分发;二者均不证明设备动画或嵌套消费。
  • 原生 Modifier 稳定 key 相等性:固定 revision 的 NativeViewElementTest.kt, 第 14–55 行。
  • AndroidView 回滚、commit 发布和 release 失败隔离:固定 revision 的 ViewTreeRenderTransactionTest.kt, 第 330–341 行和第 393–470 行。

已编译 API Sample 覆盖 Modifier 链构造、相对布局边和 AndroidView 互操作,但当前没有已编译 迁移 Sample 演示 Compose 自定义布局替代方案或真实嵌套 WindowInsets 行为。本文刻意避免嵌入 第二份未编译的事实来源。

已知缺口与复核触发条件

以下缺口仍是迁移契约的一部分:

  • 没有公开的自定义测量或 Modifier.Node 等价能力;
  • 没有已验证的 BoxScope.matchParentSize 等价能力;
  • 没有 tracked 与 static 两种 UiLocal 变体;
  • 没有嵌套 inset 消费协议;
  • 没有端到端 WindowInsets 动画或 View/ViewCompose 混合消费测试;
  • 尚无设备矩阵认证全部受支持 Android 版本上的相对 Inset 选择。

发生以下任何事件时,负责人必须复核本文:

  1. Compose Runtime、UI 或 Foundation 提升所选语义基线。
  2. 仓库 Compose 或 Kotlin 可执行基线变化。
  3. 公开布局、父数据、Modifier、环境、local、inset 或 AndroidView 契约变化。
  4. Renderer 修改 Modifier 折叠、LayoutParams 优先级、环境 rebind 或原生事务行为。
  5. 新的已编译迁移 sample 或 instrumentation 测试关闭了任一已记录缺口。

复核时必须先检查官方上游文档,再检查当前 ViewCompose 源码和测试。仓库能针对旧版 Compose artifact 构建通过,并不足以证明较新的上游语义契约没有变化。