卷 I · 认领CH 01深度 01/24

你不是从零开始:Compose 与 Flutter 的血缘

直觉过境 「声明式 UI 我懂:界面是状态的函数,状态变了就重新描述一遍。」 直接放行

这本书不打算从「什么是 Widget」讲起,因为你已经写了几年 Compose。你要的不是入门,是把一门用过一点、但心里没底的框架,变成一门你能推理的框架。好消息是:你手上有的东西比你以为的多得多。这一章先把它们一件件认领回来,然后指出剩下那些真正要学的东西藏在哪。

心智模型血缘对照总表路线图

先说一件让人放松的事

Compose 和 Flutter 不是「两个流派」,它们是近亲。两者的祖先都是 React 那套「UI = f(state)」的思想,而 Compose 在设计阶段直接参考过 Flutter——Google 内部这两个团队是互相看着做的。结果就是:把一份 Flutter 代码摆到你面前,你能读懂七成,而且读懂的那七成基本没有理解偏差

看一段最普通的 Flutter 代码,和它的 Compose 对应物:

class GreetingCard extends StatelessWidget {
  const GreetingCard({super.key, required this.name});

  final String name;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text('你好,$name', style: Theme.of(context).textTheme.titleLarge),
          const SizedBox(height: 8),
          Text('今天有 3 节课'),
        ],
      ),
    );
  }
}
@Composable
fun GreetingCard(name: String, modifier: Modifier = Modifier) {
    Column(modifier.padding(16.dp)) {
        Text("你好,$name", style = MaterialTheme.typography.titleLarge)
        Spacer(Modifier.height(8.dp))
        Text("今天有 3 节课")
    }
}

同一个东西。没有 findViewById,没有 XML,没有「先查到控件再命令它改」——你描述「在这个状态下界面长什么样」,框架负责让屏幕变成那个样子。这套心智模型你已经有了,一个字都不用改,这是这本书最大的一块地基。

可以直接搬过来的东西

下面这张表里的每一行,都是你可以今天就用的知识。名字可能变了,语义没变。

你在 Compose 里的东西Flutter 里的对应物差别
@Composable funclass ... extends StatelessWidget那边是函数,这边是类;但都是「输入状态、输出描述」
Column / Row / BoxColumn / Row / Stack连参数名都像:MainAxisAlignmentArrangement
Modifier.weight(1f)Expanded(flex: 1)两趟分配算法完全一致(第 11 章验证)
Constraints(测量协议)BoxConstraints同一套协议:约束向下、尺寸向上
CompositionLocalInheritedWidget + .of(context)都是就近取值、都是 O(1)(第 15 章验证)
MaterialTheme.colorSchemeTheme.of(context).colorSchemeMaterial 3 自 Flutter 3.16 起是默认
LazyColumn { items(…) }ListView.builder都是只建可见项 + 缓存区(第 23 章验证)
状态提升(state hoisting)一模一样的做法连「无状态组件更好测」的论证都一样
@Preview@Preview(Widget Previewer)Flutter 3.35 起可用,3.38 起 IDE 集成,仍是实验特性
Gradle / AGP / 签名那一套还是它,只是外面套了一层 flutter buildAndroid 那半边你本来就会(第 24 章)

这张表值得你多看两眼。它的意思是:你在 Flutter 面前不是新手,你是一个换了口音的熟练工。如果你之前做那点 Flutter 项目时觉得「好像会又好像不会」,多半不是因为你缺基础,而是因为下面这三件事没人给你讲透。

三处「看起来一样,其实不同」

这三处,就是这本书剩下 23 章的全部内容。现在先看一眼,不用马上懂。

一、Compose 有编译器帮你,Flutter 没有

你在 Compose 里写 var count by remember { mutableStateOf(0) },然后在某个 Text 里读它。点一下按钮,只有那个 Text 所在的重组作用域会重跑。这不是魔法,是 Compose 编译器插件在你的每个可组合函数里插了记账代码:谁读了哪个状态,状态变了就通知谁。

Flutter 没有这个编译器插件。你调用 setState,框架把整棵子树从这个组件往下重建一遍。没有细粒度订阅,没有自动划范围。

听起来像是 Flutter 更笨——但真跑起来它一点都不慢,因为「重建」在 Flutter 里比你想的便宜得多。为什么便宜,就是卷 II 要拆的东西。而「怎么把重建范围缩小」这件在 Compose 里由编译器代劳的事,在 Flutter 里变成了一门你要亲手掌握的手艺(拆 widget、加 const、把订阅点放到最小的那块)。这是卷 IV。

二、Modifier 链没有了,取而代之的是嵌套

这是从 Compose 过来最直观的不适。你习惯的是横着写:

Text("你好")
    .let { }  // 示意
Box(
    Modifier
        .padding(16.dp)
        .background(Color.Blue)
        .clickable { onTap() }
) { Text("你好") }

Flutter 里没有 Modifier,同样的事情是竖着写的——每一个装饰都是一层新的 Widget 把内层包起来:

GestureDetector(
  onTap: onTap,
  child: ColoredBox(
    color: Colors.blue,
    child: Padding(
      padding: const EdgeInsets.all(16),
      child: const Text('你好'),
    ),
  ),
)

刚开始你会觉得这是倒退:缩进金字塔、嵌套五六层、改一个属性要数括号。但请先记住一件事,它会在第 5 章变得重要:在 Flutter 里,「一切都是 Widget」不是口号,是实现方式。padding 是一个 Widget、居中是一个 Widget、点击响应是一个 Widget、甚至连「限制尺寸」都是一个 Widget。这个统一性的代价是嵌套,收益是——你只需要学一套规则,就能推理整棵树。

✎ 缩进金字塔的三个止痛药

1. 用 IDE 的重构快捷键。VS Code 里光标停在 widget 上按 Ctrl/Cmd + .,「Wrap with Padding / Center / Column」一键包一层,「Remove this widget」一键脱一层。没人手打这些嵌套。

2. 把 build 方法拆成小 widget,而不是拆成方法。_buildHeader() 这种私有方法虽然能减少缩进,但它不会给你一个新的 Element,重建时也就没法被跳过。拆成独立的 StatelessWidget 才有性能收益——这条在第 14 章会用数字证明。

3. 相信 dart formatDart 的格式化是强约定,团队里不会有人为括号吵架。顺手加个尾随逗号(trailing comma),格式化器就会自动竖排展开,可读性立刻回来。

三、你以为的「树」只有一棵,实际有三棵

这是全书的核心,也是你之前那些「说不清为什么」的统一解释。先把三个名字摆在这里:

WIDGET
你写的那些类。不可变、极轻、每帧全部扔掉重建
ELEMENT
框架维护的那棵树,你从来没直接见过它。State 挂在这里。
RENDEROBJECT
真的在量尺寸、真的在画像素的那层。建它很贵。

Compose 也有这个分层(可组合函数 → slot table 记录 → LayoutNode 树),但它把中间那层藏得很好,你不用知道也能写。Flutter 反过来——它把中间那层暴露给你,甚至要求你参与管理constKeyBuildContext,这三样你之前可能一直似懂非懂的东西,全都是「管 Element 树」的工具。

亲手看看:24 条直觉逐条过境

下面这张表就是全书的地图。每一章开头都会盖上其中一枚印章,然后用一整章把它讲清楚。你可以按判定筛选,先看看有多少东西是白拿的。

注意「得扔掉」那 9 条里,有 6 条集中在第 4 到第 14 章——它们全都指向同一个源头:你对「框架怎么知道什么变了」的想象。Compose 用编译器回答了这个问题,Flutter 用三棵树回答,答案完全不同。这也是为什么这本书用了整整一卷(卷 II,五章)来讲这一件事。

顺便说一句:为什么是 Flutter

你们要做的是一个学校的小 App,选 Flutter 的理由很实在:一份代码,Android 和 iOS 都能交付,而学校的场景(课表、通知、报名、查分)几乎没有需要贴着平台底层写的东西。对一个熟 Compose 的人来说,上手成本主要花在这本书讲的那些差异上,一两周足够。

顺带把 2026 年的版本状况钉一下,免得你按几年前的印象做决定:

  • Flutter 3.44(2026 年 5 月,Google I/O 版),Dart 3.12
  • 渲染器已经换完了。Impeller 从 3.29 起在 Android 上是默认,到 3.44 干脆移除了 Android 10+ 上的 Skia 回退——现代 Android 设备上只有 Impeller/Vulkan 这一条路。你以前听说的「Flutter 首次滑动会卡一下」(着色器编译抖动)就是 Impeller 要解决的问题,现在那个问题已经过去了。
  • Material 3 是默认主题(自 3.16),和你在 Compose 里用的是同一套设计系统。
  • Widget Previewer@Preview)从 3.35 开始可用,3.38 起 IDE 集成。目前仍是实验特性,但你熟悉的那个开发节奏回来了。
⇄ Compose 对照 · 本章小结

能直接带过来的:声明式心智模型、状态提升、Column/Row/Stack、weight ↔ Expanded、约束协议、CompositionLocal ↔ InheritedWidget、Material 3、列表懒加载、Gradle 那一套、以及 @Preview

要改的:Modifier 链 → 嵌套 Widget(用 IDE 的 wrap 快捷键,别手打)。

要扔的:「编译器会帮我把重组范围划到最小」——它不会。在 Flutter 里,划范围是你的活儿。

这一章的一句话

你不是从零开始:Compose 给你的声明式心智模型、布局协议和大部分组件直觉都能原样带过来;真正要重学的只有一件事——Compose 把 UI 的中间层藏进了编译器,而 Flutter 把它摆在桌面上,叫做 Element 树。

不过在拆那棵树之前,得先把语言过一遍。下一章两小时搞定 Dart——对一个写 Kotlin 的人来说,它几乎没有新东西,只有八个会绊脚的地方。