本节摘要:Flutter 用 Dart 语言写一切,用 Widget 树描述界面。本节讲清 Dart 的强类型、空安全、变量声明、函数与类的核心语法,再落到 Widget 树概念——界面如何组织成一棵嵌套的树,StatelessWidget 与 StatefulWidget 各管什么。Dart 语法不难,难的是把"界面是一棵树"的思维建立起来。
阅读完本节,你应当能够:
先看一个"看起来复杂"的现象:一个简单的 Flutter 界面,代码往往嵌套得很深:
return MaterialApp( home: Scaffold( body: Center( child: Text('你好 Flutter'), ), ), );
新手第一反应通常是"这也太绕了,就不能直接写一个文本框吗?"。这正是 Flutter 与大多数 UI 框架最不一样的地方——它把界面完全当成一棵树来写。每层嵌套都是树的一个节点,MaterialApp 是根,Scaffold 是页面骨架,Center 是布局,Text 是叶子。
为什么这样设计?因为 Flutter 一切皆 Widget,布局、文本、按钮、手势都是 Widget。把界面写成树,换来的是极致的统一性:想加边距就包一层 Padding,想居中就包一层 Center,想响应点击就包一层 GestureDetector。组合的威力大于单个组件的复杂性,这是 Flutter 的设计哲学。
换一个角度理解这种"层层包裹":它像搭积木。积木的世界里没有"一个特殊的大积木"之说,所有积木都是同一套系统,可以任意嵌套组合。Flutter 就是积木世界——Widget 是唯一积木,嵌套是唯一组合方式。这个一致性让 Flutter 的学习曲线在"越学越简单"的方向走:你学会一个 Widget 的用法,就等于学会了所有 Widget 的用法,因为它们遵守同一套规则。相比之下,其他框架里组件类型五花八门,每类都要单独记一套行为,心智负担更重。
Dart 由 Google 开发,是一门面向对象的强类型语言,语法与 Java、C#、Kotlin、TypeScript 高度相似。对有过任何一种面向对象语言经验的读者,学习成本很低。
变量声明。三种写法各有用途:var 让编译器推断类型;final 表示只能赋值一次;const 表示编译期常量。默认倾向 final,需要变化才用非 final 变量。
强类型。所有变量都有明确类型,编译期就能抓错误:
String name = 'Flutter'; int age = 3; double version = 3.10; bool isActive = true;
空安全。Dart 2.12 起引入空安全:默认变量不能为 null,用问号标记才允许为空:
String? nullableString; // 可能为 null String nonNullableString = 'Hello'; // 绝不为 null
空安全把"空指针异常"从运行时问题变成编译期问题。如果你用可空类型,代码必须显式处理空的情况——这强制你考虑边界,而非指望运行时兜底。
函数。函数是一等公民,能作为参数传递。Dart 的箭头函数用于单表达式函数:
String greet(String name) => 'Hello, $name!';
字符串里用 $name 直接嵌入变量,类似模板字符串的简洁。
类与对象。Dart 面向对象,支持类、继承、抽象类、接口与混入。Flutter 里你会大量接触类——Widget 本身都是类。一个简单的类长这样:
class User { final String name; final int age; User(this.name, this.age); String describe() => '$name 今年 $age 岁'; }
注意构造函数的简写:User(this.name, this.age) 直接把参数赋给同名属性,省去显式赋值代码。Dart 在很多细节上都追求"少写样板代码",这对开发效率是实打实的帮助。
对刚学完 3.1 的读者,一张 Dart 与 JavaScript 的语法对照表会很有用,能利用已有的 JS 知识加速理解 Dart:
| 概念 | JavaScript | Dart |
|---|---|---|
| 变量声明 | let / const | var / final / const |
| 类型 | 弱类型,TS 补强 | 天生强类型 |
| 空值处理 | undefined 与 null | 空安全机制(问号标记) |
| 字符串嵌入 | 模板字符串 {name} | name 直接嵌入 | |
| 函数简写 | 箭头函数 () => | 箭头函数 => |
| 类 | class | class(含混入) |
| 异步 | async/await 与 Promise | async/await 与 Future |
这张表不是让你把两种语言记成"翻译对照",而是帮你建立一种迁移思维:你已经会的东西,在新语言里往往有对应物。找到对应物,学习速度会快很多。这也是本书坚持双框架对照的原因——知识迁移是最高效的学习方式。
Widget 是什么。在 Flutter 里,一切界面元素都是 Widget:文本、按钮、布局容器、甚至整个应用。Widget 是"界面的配置描述",它描述某个界面部分应该长什么样。
树怎么组织。Widget 按父子关系嵌套,形成一棵树。父 Widget 包含子 Widget,子 Widget 再包含孙 Widget。整棵树的根通常是 MaterialApp,往下是页面、布局、具体控件。

看这棵树,再回看最上面那段"看起来复杂"的代码——它们是一一对应的。理解了树,那段代码的嵌套就不再是绕,而是必然。
Widget 分为两大类:
StatelessWidget(无状态)。创建后不维护可变状态,界面内容由传入参数决定,不会自己变化。Text、Icon 属于这一类。
StatefulWidget(有状态)。需要维护可变状态,比如输入框内容、复选框勾选、计数器的数字。它由两部分组成:Widget 本体加一个 State 对象,State 持有数据并在变化时触发界面更新。
判断一个界面该用哪种:内容会随自身状态变吗?会就 Stateful,不会就 Stateless。这个判断是 Flutter 新手最早要建立的直觉之一。
| 场景 | 用哪种 | 原因 |
|---|---|---|
| 显示静态文本 | Stateless | 内容不变 |
| 计数器按钮 | Stateful | 数字会变 |
| 文本输入框 | Stateful | 内容随输入变化 |
| 纯布局容器 | Stateless | 本身不持有状态 |
这张表的判断标准只有一句:界面内容会不会因为自身状态改变而改变。注意是"自身状态",不是"父级传进来的数据"。如果内容由父级传参决定、自己从不改变,哪怕界面很复杂,也用 Stateless。很多新手把"复杂界面"误判为"需要状态",其实复杂的只是布局嵌套,不是状态管理。
一个容易困惑的点:Widget 既然不可变,那状态存在哪里?答案是存在 State 对象里,与 Widget 分离。这套设计有两个好处。一是性能——Widget 可以被大量创建而不用担心副作用,Flutter 通过比较新旧树决定更新范围;二是可预测性——Widget 描述"此刻界面该是什么样",不负责"怎么变",职责清晰。这个"配置与状态分离"的设计,与 3.1 节 JSX 里"描述而非操作"的哲学一脉相承,两套框架殊途同归。
在 build 里做耗时操作。build 方法会被频繁调用(热重载、状态变化、尺寸变化都会触发),放耗时逻辑会卡 UI。
忘记处理空安全。可空类型不判空直接使用,编译器会报错——这不是麻烦,是保护。养成"可空就显式处理"的习惯。
滥用 StatefulWidget。很多界面其实不需要状态,用 Stateless 更简单、更好维护。原则是"无状态优先,有状态按需"。
看一个带计数器的有状态 Widget,理解 State 对象是怎么工作的:
class Counter extends StatefulWidget { const Counter({super.key}); @override State<Counter> createState() => _CounterState(); } class _CounterState extends State<Counter> { int _count = 0; void _increment() { setState(() { _count++; }); } @override Widget build(BuildContext context) { return Column( children: [ Text('点击了 $_count 次'), ElevatedButton(onPressed: _increment, child: const Text('加一')), ], ); } }
注意几个关键点:数据 _count 存在 State 对象里;修改数据必须包在 setState 里;setState 之后 build 会被重新调用,界面随之更新。setState 就是"通知框架我变了,请重画"的开关。漏掉 setState 直接改数据,界面不会变化——这是 Flutter 新手最常踩的坑,值得在第一天就记住。
听到过 Element 这个词的人可能困惑:Widget 和 Element 是什么关系?简单说,Widget 是"配置描述",Element 是"挂在树上的实际节点"。Widget 每次重建都可能生成新实例,但 Element 会被复用。这套机制是 Flutter 性能优化的基础——它让你可以大胆描述界面,而不用操心底层重建成本。对新手,记住"Widget 是图纸、Element 是施工结果"就够了,细节留到需要性能优化时再深挖。
⚠️ 常见坑:把整个界面都塞进一个 StatefulWidget。状态应该尽量下沉、局部化。状态一大,界面逻辑就乱,这会在第 5 章状态管理里被反复强调。
💡 关键直觉:Widget 是"配置描述"不是"实际绘制对象"。Flutter 会高效比较 Widget 树差异,只更新变化部分。所以大胆重建 Widget 树不可怕,框架会优化。
打开第 2 章建的 Flutter 项目,看 main.dart 里的代码,试着把它画成一棵 Widget 树:MaterialApp 在根,往下分叉到 Scaffold,再往下到 AppBar 与 body。画完再看一眼本节那张树图,对照一下。这一步看似简单,却是把"Widget 树"从概念变成直觉最快的办法。
问:不学 Dart 能直接用 Flutter 吗? 不能。Flutter 的所有代码都是 Dart 写的,绕不开。但好消息是 Dart 不难学——有面向对象语言基础的人,一周就能上手写代码。它不像 Swift 或 Kotlin 那样需要常年积累才敢写生产代码。
问:为什么 Flutter 不选 JavaScript? 因为 Flutter 需要 AOT 编译成机器码来保证性能,而 Dart 同时支持 JIT(开发期热重载)与 AOT(发布期高性能),是"一鱼两吃"。JavaScript 生态虽大,但引擎层面的控制力不如 Dart。这是技术与生态权衡后的选择。
问:StatefulWidget 是不是更"高级"? 不是。Stateless 不等于低级,Stateful 也不等于高级。两者是"是否需要可变状态"的两种选择,与难度无关。优秀代码的常态反而是大量 Stateless 加少量精心设计的 Stateful。
读 Widget 树有一个高效的办法:利用 IDE 的括号匹配与折叠。把代码里嵌套的括号折叠起来,一眼就能看到树的层级轮廓;展开某层括号,就看到该层节点的子 Widget。这个技巧让"看代码"变成"看树"。另一个辅助是给每一层注释一句"这个 Widget 负责什么",写注释的过程强迫你理清每层的职责。坚持几屏代码,Widget 树的直觉就建立了——它不再是需要"想象"的抽象,而是看得见的层级。
第 3.3 节提到 Widget 不可变,这看起来像是限制,实际是简化:你不用追踪"这个对象被改成了什么样"。Widget 每次都是"新造一个描述",没有中途被改的中间态,心智负担反而低。对比命令式 UI 里"同一个按钮对象被反复修改属性"的追踪成本,Widget 的"整树重建加差异比较"在思维上更干净。理解了这一点,你就明白 Flutter 社区为什么反复强调"别试图复用 Widget 实例"——那是在对抗框架的简化设计。
语言的皮与树的骨都认识了,下一节把两条路线收拢——声明式 UI 思想,为什么"状态驱动界面"是现代 UI 的核心。