前三课完成了环境搭建、项目创建和工具链配置。从本课开始,正式进入 UI 基础与核心组件模块。本课将深入 Composable 函数的本质、声明式与命令式的思维差异、Composable 函数的命名规范与设计原则,以及可组合项的组合与嵌套策略。本课内容基于 CMP 1.12.1。
目录
- [一、@Composable 注解的本质:编译器插件如何实现重组](#一、@Composable 注解的本质:编译器插件如何实现重组)
- [1.1 @Composable 不是注解处理器](#1.1 @Composable 不是注解处理器)
- [1.2 Composer 与三个核心职责](#1.2 Composer 与三个核心职责)
- [1.3 位置记忆化](#1.3 位置记忆化)
- [1.4 可组合项的生命周期与调用点识别](#1.4 可组合项的生命周期与调用点识别)
- [二、声明式 vs 命令式:从"如何做"到"是什么"的思维转变](#二、声明式 vs 命令式:从“如何做”到“是什么”的思维转变)
- [2.1 命令式 UI 模型的问题](#2.1 命令式 UI 模型的问题)
- [2.2 声明式 UI 的核心思想](#2.2 声明式 UI 的核心思想)
- [2.3 Composable 函数的四个关键属性](#2.3 Composable 函数的四个关键属性)
- [2.4 幂等性与无副作用](#2.4 幂等性与无副作用)
- [三、Composable 函数的命名规范与设计原则](#三、Composable 函数的命名规范与设计原则)
- [3.1 命名规范](#3.1 命名规范)
- [3.2 设计原则](#3.2 设计原则)
- 四、可组合项的组合与嵌套
- [4.1 组合优于继承](#4.1 组合优于继承)
- [4.2 Slot 模式](#4.2 Slot 模式)
- [4.3 嵌套的深度控制](#4.3 嵌套的深度控制)
- [4.4 编译期调用点识别与组合性能](#4.4 编译期调用点识别与组合性能)
- [4.5 前端开发者视角:Compose 与 React 组件模型对照](#4.5 前端开发者视角:Compose 与 React 组件模型对照)
- 五、跨平台差异与注意事项
- [5.1 共享编译器与运行时](#5.1 共享编译器与运行时)
- [5.2 平台入口差异](#5.2 平台入口差异)
- [5.3 commonMain 的约束](#5.3 commonMain 的约束)
- 配套习题(15题)
- 本课知识点总结
文末附有15道配套习题与本课知识点总结。
一、@Composable 注解的本质:编译器插件如何实现重组
1.1 @Composable 不是注解处理器
@Composable 注解与常见的注解处理器(Annotation Processor)有本质区别。它不由注解处理器生成源代码,而是由 Google 构建的 Kotlin 编译器插件直接处理,将相关逻辑编织到编译后的代码中。Compose 在 Kotlin 编译器的类型检测与代码生成阶段依赖该插件工作,因此无需注解处理器即可使用 Compose。
@Composable 函数在编译期被特殊处理的方式类似于 suspend 函数------编译器会在函数签名中注入额外的参数,并在调用点插入辅助代码,以实现自动重组和组件间上下文的隐式传递。
1.2 Composer 与三个核心职责
Compose 运行时的核心是 Composer。它作为隐式参数传递给每一个 @Composable 函数,调用则由 Compose 编译器插件插入。Composer 承担三项核心职责:
- 记录组合的位置信息(位置记忆化)------记录传递给 remember 的 lambda 函数的执行结果,以及传递给可跳过(skippable)可组合项的参数值
- 检测组合的变化------当状态改变时,识别哪些可组合项需要重新执行
- 增量求值组合------随着数据变化,只重新执行受影响的部分
下面是 Composer 三项核心职责的流程图:
#mermaid-svg-aKoahzrFV6bv5w9P{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-aKoahzrFV6bv5w9P .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-aKoahzrFV6bv5w9P .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-aKoahzrFV6bv5w9P .error-icon{fill:#552222;}#mermaid-svg-aKoahzrFV6bv5w9P .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-aKoahzrFV6bv5w9P .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-aKoahzrFV6bv5w9P .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-aKoahzrFV6bv5w9P .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-aKoahzrFV6bv5w9P .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-aKoahzrFV6bv5w9P .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-aKoahzrFV6bv5w9P .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-aKoahzrFV6bv5w9P .marker{fill:#333333;stroke:#333333;}#mermaid-svg-aKoahzrFV6bv5w9P .marker.cross{stroke:#333333;}#mermaid-svg-aKoahzrFV6bv5w9P svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-aKoahzrFV6bv5w9P p{margin:0;}#mermaid-svg-aKoahzrFV6bv5w9P .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-aKoahzrFV6bv5w9P .cluster-label text{fill:#333;}#mermaid-svg-aKoahzrFV6bv5w9P .cluster-label span{color:#333;}#mermaid-svg-aKoahzrFV6bv5w9P .cluster-label span p{background-color:transparent;}#mermaid-svg-aKoahzrFV6bv5w9P .label text,#mermaid-svg-aKoahzrFV6bv5w9P span{fill:#333;color:#333;}#mermaid-svg-aKoahzrFV6bv5w9P .node rect,#mermaid-svg-aKoahzrFV6bv5w9P .node circle,#mermaid-svg-aKoahzrFV6bv5w9P .node ellipse,#mermaid-svg-aKoahzrFV6bv5w9P .node polygon,#mermaid-svg-aKoahzrFV6bv5w9P .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-aKoahzrFV6bv5w9P .rough-node .label text,#mermaid-svg-aKoahzrFV6bv5w9P .node .label text,#mermaid-svg-aKoahzrFV6bv5w9P .image-shape .label,#mermaid-svg-aKoahzrFV6bv5w9P .icon-shape .label{text-anchor:middle;}#mermaid-svg-aKoahzrFV6bv5w9P .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-aKoahzrFV6bv5w9P .rough-node .label,#mermaid-svg-aKoahzrFV6bv5w9P .node .label,#mermaid-svg-aKoahzrFV6bv5w9P .image-shape .label,#mermaid-svg-aKoahzrFV6bv5w9P .icon-shape .label{text-align:center;}#mermaid-svg-aKoahzrFV6bv5w9P .node.clickable{cursor:pointer;}#mermaid-svg-aKoahzrFV6bv5w9P .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-aKoahzrFV6bv5w9P .arrowheadPath{fill:#333333;}#mermaid-svg-aKoahzrFV6bv5w9P .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-aKoahzrFV6bv5w9P .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-aKoahzrFV6bv5w9P .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-aKoahzrFV6bv5w9P .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-aKoahzrFV6bv5w9P .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-aKoahzrFV6bv5w9P .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-aKoahzrFV6bv5w9P .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-aKoahzrFV6bv5w9P .cluster text{fill:#333;}#mermaid-svg-aKoahzrFV6bv5w9P .cluster span{color:#333;}#mermaid-svg-aKoahzrFV6bv5w9P div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-aKoahzrFV6bv5w9P .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-aKoahzrFV6bv5w9P rect.text{fill:none;stroke-width:0;}#mermaid-svg-aKoahzrFV6bv5w9P .icon-shape,#mermaid-svg-aKoahzrFV6bv5w9P .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-aKoahzrFV6bv5w9P .icon-shape p,#mermaid-svg-aKoahzrFV6bv5w9P .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-aKoahzrFV6bv5w9P .icon-shape .label rect,#mermaid-svg-aKoahzrFV6bv5w9P .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-aKoahzrFV6bv5w9P .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-aKoahzrFV6bv5w9P .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-aKoahzrFV6bv5w9P :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} @Composable 函数被调用
Composer 作为隐式参数注入
职责一:记录组合的位置信息
职责二:检测组合的变化
职责三:增量求值组合
位置记忆化
记录 remember 结果与参数值
状态改变时
识别需重新执行的可组合项
只重新执行受影响的部分
Slot Table 存储组合信息
1.3 位置记忆化
位置记忆化(Positional Memoization)是理解 Composable 函数运行机制的关键。记忆化是一种根据函数的实际参数记住上一次调用结果的技术,而位置记忆化额外将函数在执行调用图中的位置视为隐式参数之一。
考虑以下示例:
kotlin
@Composable
fun A() {
val data = remember { Data() }
// ...
}
@Composable
fun B() {
A()
A()
}
当 B 第一次被调用时,会创建两份 Data 副本,每次 A 的调用各一份。然而当 B 再次被调用(重组期间)时,lambda 不会被再次调用,而是返回之前的值。
Composer 使用一个称为 Slot Table(槽位表) 的数据结构来存储这些信息。Slot Table 将组合信息存储在展平为两个数组的组树中:一个数组存储组的信息(如整数 Key、标志位、节点数、大小),另一个数组存储槽位值(如 remember 的调用结果)。
1.4 可组合项的生命周期与调用点识别
组合是描述界面的可组合项树结构。当 Compose 首次运行可组合项时,在初始组合期间跟踪所调用的可组合项。之后当应用状态变化时,Compose 安排重组------重新执行可能因状态更改而更改的可组合项,然后更新组合。
组合中可组合项的实例由其调用点 进行标识。Compose 编译器将每个调用点都视为不同的调用点,从多个调用站点调用可组合项会在组合中创建多个实例。在重组期间,如果某个可组合项的输入未更改,Compose 将跳过对该可组合项的调用。
kotlin
@Composable
fun LoginScreen(showError: Boolean) {
if (showError) {
LoginError()
}
LoginInput() // 此调用点影响 LoginInput 在组合中的位置
}
@Composable
fun LoginInput() { /* ... */ }
在上面的代码中,LoginScreen 有条件地调用 LoginError,并始终调用 LoginInput。每个调用都有唯一的调用点和源位置,编译器使用它们进行唯一识别。即使 LoginInput 从第一次被调用变为第二次被调用,其实例仍将在不同重组中保留,并且由于没有参数变化,Compose 会跳过对 LoginInput 的调用。
调用点与执行顺序 :多次调用同一可组合项会将其多次添加到组合中。如果从同一个调用点多次调用某个可组合项,Compose 无法仅凭调用点唯一标识每次调用,因此还会使用执行顺序来区分实例。这种行为有时是必需的,但在某些情况下会导致意外行为。
下面是可组合项生命周期与调用点识别的流程图:
#mermaid-svg-LeWSKZcKQ8SPcZ4r{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-LeWSKZcKQ8SPcZ4r .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-LeWSKZcKQ8SPcZ4r .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-LeWSKZcKQ8SPcZ4r .error-icon{fill:#552222;}#mermaid-svg-LeWSKZcKQ8SPcZ4r .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-LeWSKZcKQ8SPcZ4r .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-LeWSKZcKQ8SPcZ4r .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-LeWSKZcKQ8SPcZ4r .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-LeWSKZcKQ8SPcZ4r .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-LeWSKZcKQ8SPcZ4r .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-LeWSKZcKQ8SPcZ4r .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-LeWSKZcKQ8SPcZ4r .marker{fill:#333333;stroke:#333333;}#mermaid-svg-LeWSKZcKQ8SPcZ4r .marker.cross{stroke:#333333;}#mermaid-svg-LeWSKZcKQ8SPcZ4r svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-LeWSKZcKQ8SPcZ4r p{margin:0;}#mermaid-svg-LeWSKZcKQ8SPcZ4r .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-LeWSKZcKQ8SPcZ4r .cluster-label text{fill:#333;}#mermaid-svg-LeWSKZcKQ8SPcZ4r .cluster-label span{color:#333;}#mermaid-svg-LeWSKZcKQ8SPcZ4r .cluster-label span p{background-color:transparent;}#mermaid-svg-LeWSKZcKQ8SPcZ4r .label text,#mermaid-svg-LeWSKZcKQ8SPcZ4r span{fill:#333;color:#333;}#mermaid-svg-LeWSKZcKQ8SPcZ4r .node rect,#mermaid-svg-LeWSKZcKQ8SPcZ4r .node circle,#mermaid-svg-LeWSKZcKQ8SPcZ4r .node ellipse,#mermaid-svg-LeWSKZcKQ8SPcZ4r .node polygon,#mermaid-svg-LeWSKZcKQ8SPcZ4r .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-LeWSKZcKQ8SPcZ4r .rough-node .label text,#mermaid-svg-LeWSKZcKQ8SPcZ4r .node .label text,#mermaid-svg-LeWSKZcKQ8SPcZ4r .image-shape .label,#mermaid-svg-LeWSKZcKQ8SPcZ4r .icon-shape .label{text-anchor:middle;}#mermaid-svg-LeWSKZcKQ8SPcZ4r .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-LeWSKZcKQ8SPcZ4r .rough-node .label,#mermaid-svg-LeWSKZcKQ8SPcZ4r .node .label,#mermaid-svg-LeWSKZcKQ8SPcZ4r .image-shape .label,#mermaid-svg-LeWSKZcKQ8SPcZ4r .icon-shape .label{text-align:center;}#mermaid-svg-LeWSKZcKQ8SPcZ4r .node.clickable{cursor:pointer;}#mermaid-svg-LeWSKZcKQ8SPcZ4r .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-LeWSKZcKQ8SPcZ4r .arrowheadPath{fill:#333333;}#mermaid-svg-LeWSKZcKQ8SPcZ4r .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-LeWSKZcKQ8SPcZ4r .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-LeWSKZcKQ8SPcZ4r .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-LeWSKZcKQ8SPcZ4r .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-LeWSKZcKQ8SPcZ4r .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-LeWSKZcKQ8SPcZ4r .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-LeWSKZcKQ8SPcZ4r .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-LeWSKZcKQ8SPcZ4r .cluster text{fill:#333;}#mermaid-svg-LeWSKZcKQ8SPcZ4r .cluster span{color:#333;}#mermaid-svg-LeWSKZcKQ8SPcZ4r div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-LeWSKZcKQ8SPcZ4r .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-LeWSKZcKQ8SPcZ4r rect.text{fill:none;stroke-width:0;}#mermaid-svg-LeWSKZcKQ8SPcZ4r .icon-shape,#mermaid-svg-LeWSKZcKQ8SPcZ4r .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-LeWSKZcKQ8SPcZ4r .icon-shape p,#mermaid-svg-LeWSKZcKQ8SPcZ4r .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-LeWSKZcKQ8SPcZ4r .icon-shape .label rect,#mermaid-svg-LeWSKZcKQ8SPcZ4r .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-LeWSKZcKQ8SPcZ4r .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-LeWSKZcKQ8SPcZ4r .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-LeWSKZcKQ8SPcZ4r :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
是
否
初始组合
首次运行可组合项
跟踪所调用的可组合项
状态是否变化?
安排重组
跳过重组
重新执行可能受影响的
可组合项
输入是否未更改?
跳过该可组合项调用
重新执行并更新组合
更新组合
实例由调用点唯一标识
同一调用点多次调用用执行顺序区分
二、声明式 vs 命令式:从"如何做"到"是什么"的思维转变
2.1 命令式 UI 模型的问题
在传统的命令式 UI 编程中,开发者通过实例化 widget 树来初始化界面,通常通过膨胀 XML 布局文件实现。如需更改某个 widget,需要在该 widget 上调用 setter 以更改其内部状态。例如先通过 findViewById 找到按钮,再调用 button.text = ...、button.visibility = ...。
这种模式在页面复杂后容易出现两个问题:一是 UI 结构和状态逻辑分散在不同文件中,阅读成本高;二是状态变化后要手动更新多个 View,遗漏一个就会出现界面不同步。手动操作视图增加了出错的可能性------如果一块数据在多个地方渲染,可能会忘记更新其中一个显示它的视图。
2.2 声明式 UI 的核心思想
Compose 是声明式 UI 框架。声明式 UI 的工作方式是从概念上从头重新生成整个屏幕,然后仅应用必要的更改。这种方法避免了手动更新有状态视图层级结构的复杂性。
在声明式 UI 中,开发者只需描述"当状态为 A 时显示什么,当状态为 B 时显示什么"。状态一变,Compose 会触发重组,自动更新界面。
从概念上重新生成整个屏幕听起来代价高昂,但 Compose 会智能地选择 UI 中哪些部分需要重绘。这就是为什么理解 Composable 函数的设计原则如此重要------你的组件设计方式直接影响 Compose 能否有效跳过不必要的重组。
下面是声明式与命令式 UI 的对比图:
#mermaid-svg-rgjciEGOwShCRjAd{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-rgjciEGOwShCRjAd .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-rgjciEGOwShCRjAd .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-rgjciEGOwShCRjAd .error-icon{fill:#552222;}#mermaid-svg-rgjciEGOwShCRjAd .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-rgjciEGOwShCRjAd .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-rgjciEGOwShCRjAd .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-rgjciEGOwShCRjAd .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-rgjciEGOwShCRjAd .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-rgjciEGOwShCRjAd .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-rgjciEGOwShCRjAd .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-rgjciEGOwShCRjAd .marker{fill:#333333;stroke:#333333;}#mermaid-svg-rgjciEGOwShCRjAd .marker.cross{stroke:#333333;}#mermaid-svg-rgjciEGOwShCRjAd svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-rgjciEGOwShCRjAd p{margin:0;}#mermaid-svg-rgjciEGOwShCRjAd .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-rgjciEGOwShCRjAd .cluster-label text{fill:#333;}#mermaid-svg-rgjciEGOwShCRjAd .cluster-label span{color:#333;}#mermaid-svg-rgjciEGOwShCRjAd .cluster-label span p{background-color:transparent;}#mermaid-svg-rgjciEGOwShCRjAd .label text,#mermaid-svg-rgjciEGOwShCRjAd span{fill:#333;color:#333;}#mermaid-svg-rgjciEGOwShCRjAd .node rect,#mermaid-svg-rgjciEGOwShCRjAd .node circle,#mermaid-svg-rgjciEGOwShCRjAd .node ellipse,#mermaid-svg-rgjciEGOwShCRjAd .node polygon,#mermaid-svg-rgjciEGOwShCRjAd .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-rgjciEGOwShCRjAd .rough-node .label text,#mermaid-svg-rgjciEGOwShCRjAd .node .label text,#mermaid-svg-rgjciEGOwShCRjAd .image-shape .label,#mermaid-svg-rgjciEGOwShCRjAd .icon-shape .label{text-anchor:middle;}#mermaid-svg-rgjciEGOwShCRjAd .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-rgjciEGOwShCRjAd .rough-node .label,#mermaid-svg-rgjciEGOwShCRjAd .node .label,#mermaid-svg-rgjciEGOwShCRjAd .image-shape .label,#mermaid-svg-rgjciEGOwShCRjAd .icon-shape .label{text-align:center;}#mermaid-svg-rgjciEGOwShCRjAd .node.clickable{cursor:pointer;}#mermaid-svg-rgjciEGOwShCRjAd .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-rgjciEGOwShCRjAd .arrowheadPath{fill:#333333;}#mermaid-svg-rgjciEGOwShCRjAd .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-rgjciEGOwShCRjAd .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-rgjciEGOwShCRjAd .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-rgjciEGOwShCRjAd .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-rgjciEGOwShCRjAd .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-rgjciEGOwShCRjAd .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-rgjciEGOwShCRjAd .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-rgjciEGOwShCRjAd .cluster text{fill:#333;}#mermaid-svg-rgjciEGOwShCRjAd .cluster span{color:#333;}#mermaid-svg-rgjciEGOwShCRjAd div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-rgjciEGOwShCRjAd .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-rgjciEGOwShCRjAd rect.text{fill:none;stroke-width:0;}#mermaid-svg-rgjciEGOwShCRjAd .icon-shape,#mermaid-svg-rgjciEGOwShCRjAd .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-rgjciEGOwShCRjAd .icon-shape p,#mermaid-svg-rgjciEGOwShCRjAd .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-rgjciEGOwShCRjAd .icon-shape .label rect,#mermaid-svg-rgjciEGOwShCRjAd .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-rgjciEGOwShCRjAd .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-rgjciEGOwShCRjAd .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-rgjciEGOwShCRjAd :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 声明式 UI(Compose)
命令式 UI
实例化 widget 树
findViewById 查找视图
调用 setter 更新状态
手动同步多个 View
易遗漏更新
界面不同步
描述目标屏幕状态
状态变化触发重组
从头重新生成整个屏幕
仅应用必要的更改
智能跳过未变化部分
思维转变:从"如何做"到"是什么"
2.3 Composable 函数的四个关键属性
Composable 函数具有以下核心属性:
注解标记:所有 Composable 函数都必须有 @Composable 注解,它告知 Compose 编译器该函数意图将数据转换为 UI。
数据输入:Composable 函数可以接受参数,让应用逻辑描述 UI。例如,Greeting 组件接受一个 String 参数以便按名字问候用户。
UI 输出:函数通过调用其他 Composable 函数来发射 UI 层级结构。它不直接创建 UI 元素,而是通过调用 Text() 等 Composable 函数来描述目标屏幕状态。
无返回值:发射 UI 的 Composable 函数不需要返回任何值,因为它们描述的是目标屏幕状态而非构建 UI widget。
2.4 幂等性与无副作用
Composable 函数必须是快速、幂等且无副作用的。函数在多次以相同参数调用时应表现相同,不使用全局变量或随机数等外部值。函数描述 UI 时不应有副作用,如修改属性或全局变量。
这个约束是 Compose 能够安全跳过重组的前提。如果 Composable 函数有副作用,Compose 在跳过重组时可能会导致副作用丢失或重复执行,从而产生不一致的 UI 状态。
三、Composable 函数的命名规范与设计原则
3.1 命名规范
Compose 的 API 风格指南对 Composable 函数的命名有明确规定:
- 返回 Unit 的 @Composable 函数使用 PascalCase(大驼峰命名法),并以名词形式命名 ,就像这些函数是类型一样。例如
Greeting、LoginScreen、MovieOverview。 - 有返回值的 @Composable 函数按普通 Kotlin 函数命名,使用 camelCase(小驼峰命名法),通常为动词或动词短语。
- 文件名应以包含的主要 Composable 函数命名 ,例如
MyComposable.kt。
将返回 Unit 的 Composable 函数视为声明性实体------它们可以在组合中存在或不存在,因此遵循类的命名规则。这种命名方式使 Composable 函数在代码中具有视觉辨识度,一眼就能区分于普通函数。
3.2 设计原则
单一职责。每个 Composable 函数应只负责描述一个 UI 片段。当一个 Composable 函数变得过于复杂时,将其拆分为多个更小的可组合项。这不仅提高可读性,还能让 Compose 更精确地跳过未变化的部分。
参数驱动 。Composable 函数应通过参数接收所需数据,而非从外部环境隐式获取。参数设计应遵循以下原则:必填参数在前,可选参数在后(提供默认值);事件回调参数使用 on 前缀命名(如 onClick、onValueChange)。
状态提升。Composable 函数应尽量保持无状态。当需要状态时,将状态提升到调用方,Composable 函数只接收值并通过回调通知变化。这是 Compose 中最重要的设计模式之一,将在第8课详细展开。
无副作用 。Composable 函数中不应直接执行网络请求、文件读写、数据库操作等副作用。需要在组合期间执行的副作用应使用 LaunchedEffect、DisposableEffect 等专用 API。
四、可组合项的组合与嵌套
4.1 组合优于继承
Compose 采用组合(Composition)而非继承来构建 UI。Composable 函数通过调用其他 Composable 函数来构建界面,形成一棵可组合项树。这种设计避免了传统 View 系统中深层继承链带来的脆弱性问题。
kotlin
@Composable
fun ProfileCard(user: User) {
Card {
Column {
AvatarImage(user.avatarUrl)
UserName(user.name)
UserBio(user.bio)
}
}
}
每个子组件独立定义,可以单独预览、测试和复用。ProfileCard 不关心 AvatarImage 的内部实现,只关心它接受什么参数。
4.2 Slot 模式
Composable 函数可以通过接收 @Composable () -> Unit 类型的参数来实现 Slot 模式,让调用方决定插入什么内容。这是 Compose 中最灵活的组合模式。
kotlin
@Composable
fun AppScaffold(
title: String,
content: @Composable () -> Unit
) {
Column {
TopBar(title)
content()
}
}
// 使用
AppScaffold(title = "首页") {
Text("自定义内容")
Button(onClick = {}) { Text("按钮") }
}
Slot 模式让基础组件保持通用性,同时赋予调用方充分的定制自由。Material 3 中的 Scaffold、Button 等组件都大量使用了这种模式。
4.3 嵌套的深度控制
Composable 函数的嵌套应控制在合理深度内。过深的嵌套(如 Column 内嵌 Column 内嵌 Row 内嵌 Column)会降低可读性,也可能影响性能。当嵌套层级超过 3-4 层时,考虑提取中间层为独立的 Composable 函数。
kotlin
// ❌ 过深嵌套
@Composable
fun BadExample() {
Column {
Column {
Row {
Column {
Text("深层嵌套")
}
}
}
}
}
// ✅ 提取子组件
@Composable
fun GoodExample() {
Column {
ContentSection()
}
}
@Composable
private fun ContentSection() {
Row {
TextContent()
}
}
4.4 编译期调用点识别与组合性能
理解调用点识别对组合性能的影响至关重要。每个 Composable 调用在编译期都会生成一个唯一的调用点标识,Compose 运行时依赖这些标识来追踪重组状态。当 Composable 函数的调用结构在两次重组之间发生变化时(例如 if 分支中添加了新调用),Compose 需要重新构建受影响的子树。
kotlin
@Composable
fun ConditionalContent(showExtra: Boolean) {
Text("始终显示") // 调用点 1
if (showExtra) {
Text("条件显示") // 调用点 2
Icon(Icons.Default.Star) // 调用点 3
}
Text("始终显示(结尾)") // 调用点 4
}
当 showExtra 从 false 切换为 true 时,Compose 根据调用点识别出调用点 2 和 3 是新出现的,会将它们插入组合树中,而调用点 1 和 4 保持不变。如果改为在 if 分支外使用不同的条件组合,Compose 会重新评估每个调用点的身份,可能导致更多的重组。这就是为什么在列表渲染中应使用 key() 来帮助 Compose 稳定识别每个元素的调用点身份------当列表项被添加、删除或重排序时,没有 key 的情况下 Compose 使用执行顺序来区分实例,顺序变化会导致大面积的重新组合。
下面是编译期调用点识别与重组过程的流程图:
#mermaid-svg-CttZiSuktMyAM7Yn{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-CttZiSuktMyAM7Yn .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-CttZiSuktMyAM7Yn .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-CttZiSuktMyAM7Yn .error-icon{fill:#552222;}#mermaid-svg-CttZiSuktMyAM7Yn .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-CttZiSuktMyAM7Yn .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-CttZiSuktMyAM7Yn .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-CttZiSuktMyAM7Yn .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-CttZiSuktMyAM7Yn .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-CttZiSuktMyAM7Yn .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-CttZiSuktMyAM7Yn .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-CttZiSuktMyAM7Yn .marker{fill:#333333;stroke:#333333;}#mermaid-svg-CttZiSuktMyAM7Yn .marker.cross{stroke:#333333;}#mermaid-svg-CttZiSuktMyAM7Yn svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-CttZiSuktMyAM7Yn p{margin:0;}#mermaid-svg-CttZiSuktMyAM7Yn .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-CttZiSuktMyAM7Yn .cluster-label text{fill:#333;}#mermaid-svg-CttZiSuktMyAM7Yn .cluster-label span{color:#333;}#mermaid-svg-CttZiSuktMyAM7Yn .cluster-label span p{background-color:transparent;}#mermaid-svg-CttZiSuktMyAM7Yn .label text,#mermaid-svg-CttZiSuktMyAM7Yn span{fill:#333;color:#333;}#mermaid-svg-CttZiSuktMyAM7Yn .node rect,#mermaid-svg-CttZiSuktMyAM7Yn .node circle,#mermaid-svg-CttZiSuktMyAM7Yn .node ellipse,#mermaid-svg-CttZiSuktMyAM7Yn .node polygon,#mermaid-svg-CttZiSuktMyAM7Yn .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-CttZiSuktMyAM7Yn .rough-node .label text,#mermaid-svg-CttZiSuktMyAM7Yn .node .label text,#mermaid-svg-CttZiSuktMyAM7Yn .image-shape .label,#mermaid-svg-CttZiSuktMyAM7Yn .icon-shape .label{text-anchor:middle;}#mermaid-svg-CttZiSuktMyAM7Yn .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-CttZiSuktMyAM7Yn .rough-node .label,#mermaid-svg-CttZiSuktMyAM7Yn .node .label,#mermaid-svg-CttZiSuktMyAM7Yn .image-shape .label,#mermaid-svg-CttZiSuktMyAM7Yn .icon-shape .label{text-align:center;}#mermaid-svg-CttZiSuktMyAM7Yn .node.clickable{cursor:pointer;}#mermaid-svg-CttZiSuktMyAM7Yn .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-CttZiSuktMyAM7Yn .arrowheadPath{fill:#333333;}#mermaid-svg-CttZiSuktMyAM7Yn .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-CttZiSuktMyAM7Yn .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-CttZiSuktMyAM7Yn .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-CttZiSuktMyAM7Yn .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-CttZiSuktMyAM7Yn .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-CttZiSuktMyAM7Yn .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-CttZiSuktMyAM7Yn .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-CttZiSuktMyAM7Yn .cluster text{fill:#333;}#mermaid-svg-CttZiSuktMyAM7Yn .cluster span{color:#333;}#mermaid-svg-CttZiSuktMyAM7Yn div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-CttZiSuktMyAM7Yn .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-CttZiSuktMyAM7Yn rect.text{fill:none;stroke-width:0;}#mermaid-svg-CttZiSuktMyAM7Yn .icon-shape,#mermaid-svg-CttZiSuktMyAM7Yn .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-CttZiSuktMyAM7Yn .icon-shape p,#mermaid-svg-CttZiSuktMyAM7Yn .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-CttZiSuktMyAM7Yn .icon-shape .label rect,#mermaid-svg-CttZiSuktMyAM7Yn .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-CttZiSuktMyAM7Yn .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-CttZiSuktMyAM7Yn .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-CttZiSuktMyAM7Yn :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
是
否
编译期:每个 Composable 调用
生成唯一调用点标识
运行时:依赖调用点标识
追踪重组状态
两次重组之间
调用结构是否变化?
重新构建受影响的子树
保持原有组合树
列表渲染中
是否使用 key()?
稳定识别每个元素身份
避免错误重组
使用执行顺序区分实例
顺序变化导致大面积重组
高效跳过未变化部分
4.5 前端开发者视角:Compose 与 React 组件模型对照
对于有 React 经验的开发者,Compose 的组合模型有直接可迁移的心智模型:
| 概念 | React | Compose |
|---|---|---|
| 组件定义 | 函数返回 JSX | 函数带 @Composable 注解,无返回值 |
| 子内容传递 | props.children |
@Composable () -> Unit 类型的 Slot 参数 |
| 条件渲染 | {condition && <Component />} |
if (condition) { Component() } |
| 列表渲染 | .map() + key 属性 |
for 循环 / LazyColumn + key() |
| 状态记忆 | useState |
remember { mutableStateOf() } |
关键差异在于:React 组件返回 UI 描述(JSX),Compose 组件发射 UI(通过调用其他 Composable 函数)。React 的 JSX 是表达式,Compose 的函数调用是语句。这使得 Compose 的条件渲染更接近普通 Kotlin 控制流,不需要特殊的模板语法。
五、跨平台差异与注意事项
5.1 共享编译器与运行时
Compose Multiplatform 与 Jetpack Compose 共享 Compose 编译器和运行时,并使用相同的 API------@Composable 函数、状态管理工具(如 remember)、布局组件、修饰符和动画支持。Composable 函数的基础机制在 Android、iOS、Desktop 和 Web 上是完全一致的。
5.2 平台入口差异
各平台通过不同的入口点启动共享的 Composable 函数:
- Android :ComponentActivity 的
setContent {}块挂载根 Composable - iOS :
ComposeUIViewController { App() }工厂函数 - Desktop :
application { Window { App() } }启动 - Web :
ComposeViewport(document.body!!) { App() }
下面是 Compose Multiplatform 跨平台入口架构图:
#mermaid-svg-YRgFA7qHfmMvzw5w{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-YRgFA7qHfmMvzw5w .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-YRgFA7qHfmMvzw5w .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-YRgFA7qHfmMvzw5w .error-icon{fill:#552222;}#mermaid-svg-YRgFA7qHfmMvzw5w .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-YRgFA7qHfmMvzw5w .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-YRgFA7qHfmMvzw5w .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-YRgFA7qHfmMvzw5w .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-YRgFA7qHfmMvzw5w .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-YRgFA7qHfmMvzw5w .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-YRgFA7qHfmMvzw5w .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-YRgFA7qHfmMvzw5w .marker{fill:#333333;stroke:#333333;}#mermaid-svg-YRgFA7qHfmMvzw5w .marker.cross{stroke:#333333;}#mermaid-svg-YRgFA7qHfmMvzw5w svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-YRgFA7qHfmMvzw5w p{margin:0;}#mermaid-svg-YRgFA7qHfmMvzw5w .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-YRgFA7qHfmMvzw5w .cluster-label text{fill:#333;}#mermaid-svg-YRgFA7qHfmMvzw5w .cluster-label span{color:#333;}#mermaid-svg-YRgFA7qHfmMvzw5w .cluster-label span p{background-color:transparent;}#mermaid-svg-YRgFA7qHfmMvzw5w .label text,#mermaid-svg-YRgFA7qHfmMvzw5w span{fill:#333;color:#333;}#mermaid-svg-YRgFA7qHfmMvzw5w .node rect,#mermaid-svg-YRgFA7qHfmMvzw5w .node circle,#mermaid-svg-YRgFA7qHfmMvzw5w .node ellipse,#mermaid-svg-YRgFA7qHfmMvzw5w .node polygon,#mermaid-svg-YRgFA7qHfmMvzw5w .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-YRgFA7qHfmMvzw5w .rough-node .label text,#mermaid-svg-YRgFA7qHfmMvzw5w .node .label text,#mermaid-svg-YRgFA7qHfmMvzw5w .image-shape .label,#mermaid-svg-YRgFA7qHfmMvzw5w .icon-shape .label{text-anchor:middle;}#mermaid-svg-YRgFA7qHfmMvzw5w .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-YRgFA7qHfmMvzw5w .rough-node .label,#mermaid-svg-YRgFA7qHfmMvzw5w .node .label,#mermaid-svg-YRgFA7qHfmMvzw5w .image-shape .label,#mermaid-svg-YRgFA7qHfmMvzw5w .icon-shape .label{text-align:center;}#mermaid-svg-YRgFA7qHfmMvzw5w .node.clickable{cursor:pointer;}#mermaid-svg-YRgFA7qHfmMvzw5w .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-YRgFA7qHfmMvzw5w .arrowheadPath{fill:#333333;}#mermaid-svg-YRgFA7qHfmMvzw5w .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-YRgFA7qHfmMvzw5w .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-YRgFA7qHfmMvzw5w .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-YRgFA7qHfmMvzw5w .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-YRgFA7qHfmMvzw5w .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-YRgFA7qHfmMvzw5w .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-YRgFA7qHfmMvzw5w .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-YRgFA7qHfmMvzw5w .cluster text{fill:#333;}#mermaid-svg-YRgFA7qHfmMvzw5w .cluster span{color:#333;}#mermaid-svg-YRgFA7qHfmMvzw5w div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-YRgFA7qHfmMvzw5w .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-YRgFA7qHfmMvzw5w rect.text{fill:none;stroke-width:0;}#mermaid-svg-YRgFA7qHfmMvzw5w .icon-shape,#mermaid-svg-YRgFA7qHfmMvzw5w .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-YRgFA7qHfmMvzw5w .icon-shape p,#mermaid-svg-YRgFA7qHfmMvzw5w .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-YRgFA7qHfmMvzw5w .icon-shape .label rect,#mermaid-svg-YRgFA7qHfmMvzw5w .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-YRgFA7qHfmMvzw5w .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-YRgFA7qHfmMvzw5w .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-YRgFA7qHfmMvzw5w :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 共享代码 commonMain
@Composable 函数
App()
MaterialTheme
HomeScreen
Android 入口
setContent { App() }
iOS 入口
ComposeUIViewController { App() }
Desktop 入口
application { Window { App() } }
Web 入口
ComposeViewport { App() }
共享 Compose 编译器与运行时
5.3 commonMain 的约束
在 commonMain 中编写的 Composable 函数不能依赖平台特有的 API。仅限 Android 的组件(如与 Android 平台紧密相关的 API)无法在通用 Compose Multiplatform 代码中使用。例如,androidx.activity.compose.setContent、androidx.compose.material3.NavigationSuiteScaffold 等 API 是 Android 专属的,不能出现在 commonMain 中。共享代码里不能随意依赖 Android 专属 API,平台特有的 API 必须放在对应源集中。
kotlin
// ✅ commonMain 中的共享 Composable
@Composable
fun App() {
MaterialTheme {
HomeScreen()
}
}
// ✅ androidMain 中的平台入口
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent { App() } // 调用共享 Composable
}
}
配套习题(15题)
第一组:@Composable 注解本质(4题)
第1题 @Composable 注解在编译期由什么处理?( )
A. 注解处理器(Annotation Processor)生成源代码
B. Kotlin 编译器插件直接将逻辑编织到编译后的代码中
C. 反射机制在运行时解析
D. Gradle 插件在构建后生成字节码
答案:B
解析:@Composable 注解不由注解处理器生成源代码,而是由 Google 构建的 Kotlin 编译器插件直接处理,将相关逻辑编织到编译后的代码中。Compose 在 Kotlin 编译器的类型检测与代码生成阶段依赖该插件工作,因此无需注解处理器即可使用。
第2题 Composer 作为隐式参数传递给每个 @Composable 函数,它不承担以下哪项职责?( )
A. 记录组合的位置信息
B. 检测组合的变化
C. 增量求值组合
D. 管理 Gradle 构建任务
答案:D
解析:Composer 承担三项核心职责:记录组合的位置信息(位置记忆化)、检测组合的变化、增量求值组合。管理 Gradle 构建任务是 Gradle 的职责,与 Composer 无关。
第3题 位置记忆化(Positional Memoization)与普通记忆化的区别在于( )
A. 位置记忆化只在 Android 平台上可用
B. 位置记忆化额外将函数在执行调用图中的位置视为隐式参数
C. 位置记忆化不使用实际参数
D. 位置记忆化只能用于 remember 函数
答案:B
解析:位置记忆化是记忆化技术的一种,额外将函数在执行调用图中的位置视为隐式参数之一。当位置相同时,使用之前 lambda 的结果而非再次调用。
第4题 在以下代码中,当 B 第二次被调用(重组期间)时,会发生什么?
kotlin
@Composable fun A() { val data = remember { Data() } }
@Composable fun B() { A(); A() }
A. 再次创建两份新的 Data 副本
B. lambda 不被再次调用,返回之前的值
C. 抛出异常
D. 只创建一份新的 Data 副本
答案:B
解析:当 B 第一次被调用时,会创建两份 Data 副本。然而当 B 再次被调用时,lambda 不会被再次调用,而是返回之前的值。这正是位置记忆化的效果------Composer 通过 Slot Table 记录了上次的结果,在位置相同时复用。
第二组:声明式 vs 命令式(4题)
第5题 命令式 UI 模型中,更改某个 widget 的方式是( )
A. 重新生成整个屏幕
B. 在该 widget 上调用 setter 更改其内部状态
C. 通过 Compose 编译器自动更新
D. 删除并重建 widget
答案:B
解析:在命令式 UI 模型中,如需更改某个 widget,开发者在该 widget 上调用 setter 以更改其内部状态。例如调用 button.setText(String) 或 img.setImageBitmap(Bitmap)。
第6题 声明式 UI 的核心工作方式是( )
A. 通过 findViewById 查找视图并手动更新
B. 从概念上从头重新生成整个屏幕,然后仅应用必要的更改
C. 使用 XML 布局文件描述界面
D. 通过反射机制动态修改视图属性
答案:B
解析:声明式 UI 的工作方式是从概念上从头重新生成整个屏幕,然后仅应用必要的更改。这种方法避免了手动更新有状态视图层级结构的复杂性。Compose 会智能地选择 UI 中哪些部分需要重绘。
第7题 以下哪项不是 Composable 函数的关键属性?( )
A. 使用 @Composable 注解标记
B. 接受参数作为数据输入
C. 返回 View 对象
D. 通过调用其他 Composable 函数发射 UI
答案:C
解析:Composable 函数的四个关键属性是:使用 @Composable 注解标记、接受参数作为数据输入、通过调用其他 Composable 函数发射 UI 层级、无返回值。发射 UI 的 Composable 函数不需要返回任何值,因为它们描述的是目标屏幕状态而非构建 UI widget。
第8题 Composable 函数必须是幂等且无副作用的,主要原因是( )
A. Compose 编译器强制要求所有函数都无副作用
B. 这是 Compose 能安全跳过重组的前提,有副作用可能导致跳过时副作用丢失或重复
C. Kotlin 语言本身要求所有函数幂等
D. 无副作用可以提高编译速度
答案:B
解析:Composable 函数必须是快速、幂等且无副作用的。这个约束是 Compose 能够安全跳过重组的前提。如果 Composable 函数有副作用,Compose 在跳过重组时可能会导致副作用丢失或重复执行,从而产生不一致的 UI 状态。
第三组:命名规范与设计原则(3题)
第9题 返回 Unit 的 @Composable 函数应使用什么命名方式?( )
A. camelCase,以动词开头
B. PascalCase,以名词形式命名
C. snake_case
D. 全大写
答案:B
解析:返回 Unit 的 @Composable 函数使用 PascalCase(大驼峰命名法),并以名词形式命名,就像这些函数是类型一样。例如 Greeting、LoginScreen、MovieOverview。
第10题 关于有返回值的 @Composable 函数的命名,正确的是( )
A. 使用 PascalCase,以名词命名
B. 按普通 Kotlin 函数命名,使用 camelCase,通常为动词或动词短语
C. 必须使用全大写
D. 必须以 "Composable" 结尾
答案:B
解析:有返回值的 @Composable 函数按普通 Kotlin 函数命名,使用 camelCase(小驼峰命名法),通常为动词或动词短语。只有返回 Unit 的 Composable 函数才使用 PascalCase。
第11题 关于 Composable 函数的参数设计,以下做法正确的是( )
A. 必填参数放在最后,可选参数放在最前
B. 事件回调参数使用 "on" 前缀命名,如 onClick、onValueChange
C. 所有参数都必须有默认值
D. Composable 函数不应接受任何参数
答案:B
解析:事件回调参数应使用 "on" 前缀命名(如 onClick、onValueChange)。参数设计的一般原则是必填参数在前,可选参数在后(提供默认值)。Composable 函数通过参数接收数据,而非从外部环境隐式获取。
第四组:组合与嵌套(4题)
第12题 Compose 中 Slot 模式的实现方式是( )
A. 使用继承创建子类
B. 通过接收 @Composable () -> Unit 类型的参数,让调用方决定插入什么内容
C. 使用 XML 布局文件定义插槽
D. 通过全局变量传递子内容
答案:B
解析 :Composable 函数可以通过接收 @Composable () -> Unit 类型的参数来实现 Slot 模式,让调用方决定插入什么内容。这是 Compose 中最灵活的组合模式。Material 3 中的 Scaffold、Button 等组件都大量使用了这种模式。
第13题 以下关于 Composable 函数调用点识别的说法,正确的是( )
A. 所有 Composable 调用共享同一个调用点标识
B. 每个调用点在编译期生成唯一的标识,Compose 编译器将每个调用点视为不同的调用点
C. 调用点标识在运行时动态生成
D. 只有顶层 Composable 函数才有调用点标识
答案:B
解析:Compose 编译器将每个调用点都视为不同的调用点。从多个调用站点调用可组合项会在组合中创建多个可组合项实例。如果某一可组合项多次被调用,在组合中将放置多个实例,每次调用在组合中都有自己的生命周期。
第14题 在列表渲染中,使用 key() 的主要目的是( )
A. 提高列表的渲染速度
B. 帮助 Compose 稳定识别每个元素的调用点身份,避免因顺序变化导致的错误重组
C. 强制 Compose 重新渲染所有列表项
D. 减少列表项的内存占用
答案:B
解析:没有 key 的情况下,Compose 使用执行顺序来区分同一调用点的多个实例。当列表项被添加、删除或重排序时,顺序变化会导致 Compose 错误识别实例,造成大面积的重新组合。使用 key() 可以帮助 Compose 稳定识别每个元素的身份。
第15题 关于 Compose Multiplatform 中 commonMain 的 Composable 函数,以下说法正确的是( )
A. 可以直接使用 Android 的 Context API
B. 不能依赖平台特有的 API,平台特有的 API 必须放在对应源集中
C. 可以使用 androidx.activity.compose.setContent
D. 只能包含无参数的 Composable 函数
答案:B
解析:在 commonMain 中编写的 Composable 函数不能依赖平台特有的 API。仅限 Android 的组件(如 androidx.activity.compose.setContent)无法在通用 Compose Multiplatform 代码中使用。平台特有的 API 必须放在对应源集中。
本课知识点总结
1. @Composable 注解的编译机制
@Composable 由 Kotlin 编译器插件处理(非注解处理器),在编译期向函数签名注入 Composer 隐式参数。Composer 承担三项核心职责:位置记忆化、变化检测、增量求值。位置记忆化将函数在调用图中的位置视为隐式参数,通过 Slot Table 存储 remember 结果和可跳过参数值。组合中可组合项的实例由调用点唯一标识,输入未变时 Compose 会跳过重组。
2. 声明式 vs 命令式思维转变
命令式 UI 通过 findViewById 查找视图并调用 setter 手动更新,问题在于 UI 结构和状态逻辑分散、容易遗漏更新。声明式 UI 从概念上重新生成整个屏幕后仅应用必要更改。Composable 函数必须无返回值、幂等、无副作用,这是安全跳过重组的前提。Composable 函数通过调用其他 Composable 函数发射 UI 层级,而非返回 View 对象。
3. 命名规范与设计原则
返回 Unit 的 Composable 函数使用 PascalCase 并以名词命名,有返回值的按普通 Kotlin 函数命名。设计原则包括:单一职责、参数驱动(on 前缀回调)、状态提升、无副作用。文件名以主要 Composable 函数命名。
4. 组合与嵌套策略
Compose 采用组合而非继承构建 UI。Slot 模式通过 @Composable () -> Unit 参数让调用方注入内容,是最灵活的组合模式。嵌套深度应控制在 3-4 层内,过深时提取中间层为独立函数。调用点在编译期生成唯一标识,列表渲染中应使用 key() 稳定元素身份。React 开发者可将 props.children 对应为 Slot 参数、.map() + key 对应为 for 循环 + key()。
5. 跨平台差异
CMP 与 Jetpack Compose 共享编译器和运行时,Composable 函数基础机制一致。各平台入口不同(Android 用 setContent、iOS 用 ComposeUIViewController、Desktop 用 application + Window、Web 用 ComposeViewport)。commonMain 中的 Composable 函数不能依赖平台特有 API(如 androidx.activity.compose.setContent、NavigationSuiteScaffold 等)。
下一课预告:第5课将深入布局系统------Column、Row、Box 三大基础布局容器,Modifier 链的机制与顺序敏感性,尺寸约束与对齐排列的灵活运用,以及 Compose 布局与 CSS Flexbox 的对照理解。