Flutter Base Widgets 第一章:基础概念与底层机制
一、Widget 到底是什么
Flutter 官方对 Widget 的定义可以理解为:
Widget 是一份不可变的 UI 配置说明。
例如:
const Text(
'Hello',
style: TextStyle(
fontSize: 18,
color: Colors.blue,
),
);
这段代码描述了:
- 显示什么文字;
- 使用多大的字号;
- 使用什么颜色。
但 Text Widget 本身并不直接负责:
- 计算文字尺寸;
- 决定文字在屏幕上的位置;
- 把文字绘制到屏幕上;
- 保存长期可变状态。
Widget 更像一张设计说明书,而不是最终显示在屏幕上的实体。
Flutter 要求 Widget 本身不可变,所以 Widget 中的属性通常都应该是 final。需要保存可变状态时,应该把状态放在单独的 State 对象、状态管理对象或其他可变对象中。
class UserCard extends StatelessWidget {
const UserCard({
super.key,
required this.name,
});
final String name;
@override
Widget build(BuildContext context) {
return Text(name);
}
}
下面这种写法不符合 Widget 的设计:
class WrongUserCard extends StatelessWidget {
WrongUserCard({super.key});
// 错误:在 Widget 中保存可变状态。
int count = 0;
@override
Widget build(BuildContext context) {
return Text('$count');
}
}
即使代码在部分情况下能够运行,也可能因为 Widget 被重新创建而丢失数据。
二、为什么 Flutter 中几乎所有内容都是 Widget
Flutter 使用组合,而不是给一个组件加入大量功能参数。
例如,一个带有间距、背景、圆角、文字和点击效果的卡片,可以由多个 Widget 组合:
Material(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
child: InkWell(
borderRadius: BorderRadius.circular(12),
onTap: () {},
child: const Padding(
padding: EdgeInsets.all(16),
child: Text('View Profile'),
),
),
);
每个 Widget 只负责一部分工作:
Material
负责 Material 表面和水波纹绘制环境
InkWell
负责点击和水波纹反馈
Padding
负责内边距
Text
负责文字描述
Flutter 使用相同的 Widget 概念表示:
- 布局;
- 绘制;
- 交互;
- 状态;
- 动画;
- 主题;
- 导航。
这种设计使小型、单一职责的 Widget 可以组合成复杂页面。
三、Widget、Element 和 RenderObject 的区别
这是理解 Flutter 的关键。
Widget
↓ 创建或更新
Element
↓ 管理
RenderObject
↓ 执行
Layout / Paint
1. Widget:配置说明
Widget 的特点:
- 不可变;
- 创建成本较低;
- build 时可以频繁创建;
- 描述页面应该是什么样子;
- 不代表屏幕上的长期实体。
例如:
Text(
username,
style: const TextStyle(fontSize: 18),
);
2. Element:组件树中的实际位置
Element 可以理解为:
某个 Widget 在组件树中特定位置上的长期实例。
Element 负责:
- 保存当前 Widget;
- 维护父子关系;
- 连接 Widget 与 RenderObject;
- 管理 StatefulWidget 对应的 State;
- 判断新 Widget 是否能够更新旧 Element;
- 提供 BuildContext。
Flutter 每次 build 可能创建很多新 Widget,但 Element 树通常不会全部销毁重建。
Element 树会尽量跨帧保留。Flutter会比较新旧 Widget,只更新发生变化的部分,因此 Widget 可以频繁创建,而底层对象不必全部重新创建。
3. RenderObject:真正负责布局和绘制
RenderObject 负责更底层的工作:
- 接收父组件约束;
- 计算自己的尺寸;
- 布置子节点位置;
- 绘制内容;
- 处理点击测试;
- 提供语义和无障碍信息。
例如:
Text Widget
↓
RichText Widget
↓
RenderParagraph
RenderParagraph 才是实际计算文字排版并绘制文字的对象。
再例如:
Image Widget
↓
RawImage
↓
RenderImage
Flutter 在 build 阶段创建或更新 Element,并由相关的 RenderObjectElement 管理 RenderObject。RenderObject 负责最终的布局和绘制。
4. 三者对比
| 对象 | 主要作用 | 是否经常创建 | 是否保存长期状态 |
|---|---|---|---|
| Widget | 描述 UI 配置 | 是 | 否 |
| Element | 管理组件在树中的位置 | 通常不会频繁重建 | 是 |
| RenderObject | 布局、绘制、点击测试 | 通常长期复用 | 保存渲染相关状态 |
| State | 保存 StatefulWidget 的可变状态 | 通常长期复用 | 是 |
四、一个 Widget 是如何显示到屏幕上的
假设代码是:
Container(
color: Colors.blue,
child: const Text('Hello'),
);
大致过程如下:
1. Flutter 调用 build()
2. build 返回 Container 和 Text 等 Widget
3. Flutter 将 Widget 与已有 Element 对比
4. 可以复用时,更新原 Element
5. 不可以复用时,创建新的 Element
6. Element 创建或更新对应的 RenderObject
7. RenderObject 执行 layout
8. RenderObject 执行 paint
9. 合成并显示到屏幕
Container 本身是一个组合型 Widget。根据它设置的参数,内部可能进一步组合 ColoredBox、Padding、Align、ConstrainedBox 等组件。
所以在 Flutter Inspector 中看到的组件树,可能比源代码中写出来的层级更深。Flutter 官方架构文档也以 Container 为例说明,它会根据参数插入其他 Widget。
五、Widget 重新创建,不等于整个页面重新渲染
这是初学 Flutter 时最容易误解的地方。
假设:
@override
Widget build(BuildContext context) {
return Column(
children: [
Text('Count: $count'),
const Text('This text never changes'),
],
);
}
执行:
setState(() {
count++;
});
Flutter 会再次调用当前 State 的 build(),创建新的:
- Column Widget;
- 第一个 Text Widget;
- 可能复用的 const Text Widget。
但这不代表:
- 整个页面 Element 全部销毁;
- 所有 RenderObject 全部重新创建;
- 每个像素都一定重新绘制;
- 所有子组件的 State 都会丢失。
实际过程是:
新的 Widget 树
↓ 比较
原来的 Element 树
↓ 更新变化部分
RenderObject 树
Element 会根据 Widget 的类型和 Key,判断是更新原节点还是替换节点。
因此应该区分以下概念:
Widget rebuild
重新执行 build(),返回新的 Widget 配置。
Element update
已有 Element 接收到新的 Widget 配置。
Layout
RenderObject 重新计算尺寸和位置。
Paint
RenderObject 重新绘制视觉内容。
Composite
重新组合不同的绘制图层。
它们不是同一件事。
一次 build 可能没有触发布局,也可能没有触发绘制。例如,只要新配置与旧配置没有产生影响,底层 RenderObject 就不一定需要重新工作。
六、build() 方法什么时候会执行
build() 并不是只执行一次。
常见触发场景包括:
1. Widget 第一次加入组件树
const UserCard();
Flutter 创建 Element 后,会调用对应的 build()。
2. 父组件重新提供了新的配置
UserCard(
name: currentName,
);
currentName 改变后,父组件重新 build,并向原 Element 提供新的 UserCard Widget。
3. 调用 setState()
setState(() {
count++;
});
当前 State 被标记为需要重新 build。
4. 依赖的 InheritedWidget 发生变化
例如:
Theme.of(context)
MediaQuery.of(context)
Localizations.of(context, ...)
当依赖的数据发生变化时,对应组件可能重新 build。
StatelessWidget 的 build 通常会在首次插入、父组件配置发生变化或依赖的 InheritedWidget 更新时执行。
七、build() 应该保持快速且没有副作用
Flutter 可能在需要时频繁调用 build,甚至理论上可以每个渲染帧调用一次。因此 build 应该:
- 快速;
- 可重复执行;
- 只负责根据当前状态生成 UI;
- 不直接发送请求;
- 不直接写数据库;
- 不直接显示弹窗;
- 不创建需要长期管理的 Controller。
Flutter 将声明式 UI 概括为:
UI = f(state)
即:
当前 UI 是当前状态的函数
官方要求 build 应尽量没有副作用,并能够安全地被框架重复调用。
错误示例:在 build 中请求网络
@override
Widget build(BuildContext context) {
loadUsers();
return const CircularProgressIndicator();
}
问题表现
只要 build 再次执行,就可能再次请求接口:
build
→ 请求接口
→ 状态改变
→ build
→ 再次请求接口
可能造成:
- 重复请求;
- 页面闪烁;
- 数据覆盖;
- 死循环;
- 服务器压力增加。
正确写法
class UserPage extends StatefulWidget {
const UserPage({super.key});
@override
State<UserPage> createState() => _UserPageState();
}
class _UserPageState extends State<UserPage> {
late final Future<List<String>> usersFuture;
@override
void initState() {
super.initState();
usersFuture = loadUsers();
}
@override
Widget build(BuildContext context) {
return FutureBuilder<List<String>>(
future: usersFuture,
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const CircularProgressIndicator();
}
return Text(snapshot.data!.join(', '));
},
);
}
Future<List<String>> loadUsers() async {
return ['Alice', 'Bob'];
}
}
八、父组件 rebuild,子组件是否一定 rebuild
不一定。
假设:
class ParentPage extends StatefulWidget {
const ParentPage({super.key});
@override
State<ParentPage> createState() => _ParentPageState();
}
class _ParentPageState extends State<ParentPage> {
int count = 0;
@override
Widget build(BuildContext context) {
debugPrint('Parent build');
return Column(
children: [
Text('Count: $count'),
const StaticChild(),
ElevatedButton(
onPressed: () {
setState(() {
count++;
});
},
child: const Text('Add'),
),
],
);
}
}
class StaticChild extends StatelessWidget {
const StaticChild({super.key});
@override
Widget build(BuildContext context) {
debugPrint('StaticChild build');
return const Text('Static content');
}
}
父组件第一次显示时:
Parent build
StaticChild build
点击按钮后,父组件重新 build。
由于:
const StaticChild()
可以返回同一个 Widget 实例,Flutter能够跳过不少不必要的更新工作。
但不能简单理解为:
加了 const 后,子组件的 build 永远不会再执行。
如果子组件依赖的 Theme、MediaQuery、Localizations 等数据发生变化,它仍可能需要重新 build。
更准确的理解是:
const让相同配置可以复用同一个 Widget 实例,从而帮助 Flutter跳过一部分比较和重建工作。
Flutter 官方建议尽可能使用 const Widget,因为相同实例可以让框架快速跳过大量重建工作。
九、StatelessWidget 与 StatefulWidget 的本质区别
1. StatelessWidget
StatelessWidget 适合 UI 只依赖以下内容的场景:
- 构造参数;
- BuildContext;
- Theme;
- MediaQuery;
- 外部传入的状态。
例如:
class UserName extends StatelessWidget {
const UserName({
super.key,
required this.name,
});
final String name;
@override
Widget build(BuildContext context) {
return Text(
name,
style: Theme.of(context).textTheme.titleMedium,
);
}
}
StatelessWidget 并不是“页面永远不会变化”。
它可以接收不断变化的外部数据:
UserName(name: currentName)
只要父组件传入新的 currentName,它就可以显示新内容。
StatelessWidget 的真正含义是:
它自己没有需要单独维护的可变 State 对象。
2. StatefulWidget
StatefulWidget 适合需要保存组件内部可变状态的场景,例如:
- 当前计数;
- 输入框 Controller;
- FocusNode;
- AnimationController;
- TabController;
- 页面加载状态;
- 展开或收起状态。
class CounterBox extends StatefulWidget {
const CounterBox({super.key});
@override
State<CounterBox> createState() => _CounterBoxState();
}
class _CounterBoxState extends State<CounterBox> {
int count = 0;
@override
Widget build(BuildContext context) {
return Column(
children: [
Text('Count: $count'),
ElevatedButton(
onPressed: () {
setState(() {
count++;
});
},
child: const Text('Add'),
),
],
);
}
}
需要注意:
StatefulWidget 本身仍然是不可变的
真正可变的是:
_CounterBoxState
Flutter 将 StatefulWidget 与 State 分开,是为了让 Widget 可以被重新创建,同时让 State 继续绑定在对应的 Element 上。Flutter 官方也明确说明 StatefulWidget 自身不可变,可变状态存放在独立的 State 或它订阅的其他对象中。
十、为什么 Widget 必须不可变
假设 Widget 可以随意修改:
widget.name = 'Bob';
Flutter 将难以判断:
- 哪个属性发生了变化;
- 哪个 Element 需要更新;
- 哪个 RenderObject 需要重新布局;
- 哪个区域需要重新绘制;
- 修改发生在什么时候;
- 多个位置复用相同 Widget 时如何处理。
Flutter 采用的方式是:
不要修改旧 Widget
而是创建一个描述新状态的新 Widget
例如:
setState(() {
name = 'Bob';
});
下一次 build:
Text(name)
从:
Text('Alice')
变成:
Text('Bob')
Flutter 再比较新旧配置并更新 Element 和 RenderObject。
这种方式使 UI 更新过程更容易预测。
十一、组件层级很深,是否一定影响性能
不一定。
Flutter 本身就是通过大量小 Widget 进行组合。正常的:
- Padding;
- Align;
- SizedBox;
- Row;
- Column;
- Builder;
通常不是主要性能问题。
比“Widget 数量多”更值得注意的是:
- build 中进行大量计算;
- 一次创建大量不可见列表项;
- 大面积不必要 rebuild;
- 频繁使用 IntrinsicHeight;
- 频繁裁剪和透明合成;
- 大量图片没有限制解码尺寸;
- 在 build 中创建 Controller;
- 嵌套多个 shrinkWrap 列表;
- 自定义绘制不断重绘复杂内容。
Widget 只是轻量配置对象。真正成本通常来自:
- Element 管理;
- RenderObject 布局;
- 绘制;
- 图片解码;
- 图层合成;
- 大量业务计算。
因此不要为了减少一个 Padding 或 SizedBox,把代码写得难以理解。
十二、什么时候应该拆分 Widget
适合拆分的情况
1. 一段 UI 有明确业务含义
UserProfileHeader()
MembershipBadge()
MessageInputBar()
比:
buildTopPart()
buildGoldIcon()
buildBottomInput()
更容易管理和复用。
2. 某部分会独立更新
例如:
Column(
children: [
const ProfileHeader(),
MessageList(messages: messages),
MessageInputBar(
onSend: sendMessage,
),
],
);
消息输入框状态变化时,不需要把所有逻辑都塞在一个大型 build 中。
3. build 方法过长
如果一个 build 有几百行,通常应该按照页面结构拆分。
4. 某部分需要自己的生命周期
例如包含:
- TextEditingController;
- AnimationController;
- ScrollController;
- FocusNode。
此时适合拆成独立 StatefulWidget。
5. 某部分需要复用
例如约会 App 中多个页面都使用:
UserAvatar()
OnlineIndicator()
PremiumBadge()
不需要过度拆分的情况
下面这种简单代码未必需要创建新 Widget:
const SizedBox(height: 8)
也不需要为了每一个 Text 都单独建立一个文件。
拆分的目标应该是:
提高可读性
明确职责
控制更新范围
便于测试和复用
而不是单纯追求文件数量。
Flutter 官方也建议优先把可复用 UI 拆成 Widget,而不是仅使用返回 Widget 的普通辅助方法。独立 Widget 能让框架更准确地控制更新范围,并能够使用 const 优化。
十三、Flutter 最重要的布局规则
必须记住:
Constraints go down, sizes go up, parents set positions.
中文可以理解为:
父组件向子组件传递约束
子组件在约束范围内决定自己的尺寸
子组件把尺寸返回给父组件
父组件决定子组件的位置
官方文档说明,一个布局约束由四个值组成:
minWidth
maxWidth
minHeight
maxHeight
父组件先给子组件约束,子组件在约束范围内选择自己的尺寸,最后由父组件决定子组件的位置。
1. 为什么设置 width: 500 不一定显示 500
Container(
width: 500,
height: 100,
color: Colors.blue,
);
假设父组件只允许最大宽度 300:
minWidth = 300
maxWidth = 300
子组件即使想要 500,也只能显示 300。
因为:
父组件约束优先于子组件想要的尺寸
2. 为什么放进 Center 后可能显示指定宽度
Center(
child: Container(
width: 200,
height: 100,
color: Colors.blue,
),
);
Center 自己通常占据父组件允许的空间,然后给 child 一个相对宽松的约束。
只要屏幕允许,child 就可以选择:
width = 200
height = 100
3. 紧约束
紧约束表示最小值和最大值相同:
const BoxConstraints(
minWidth: 300,
maxWidth: 300,
minHeight: 100,
maxHeight: 100,
);
子组件没有选择空间,只能是:
300 × 100
4. 松约束
松约束通常表示最小值为 0,但有最大值:
const BoxConstraints(
minWidth: 0,
maxWidth: 300,
minHeight: 0,
maxHeight: 500,
);
子组件可以在范围内选择尺寸。
5. 无界约束
例如:
maxHeight = infinity
表示父组件没有给出有限的最大高度。
常见于:
- ListView 的滚动方向;
- SingleChildScrollView 的滚动方向;
- Row 对非 flex 子组件的水平方向;
- Column 对非 flex 子组件的垂直方向。
无界不代表子组件可以随意使用 double.infinity。
如果父组件说:
你的高度没有上限
子组件又说:
我要占据无限高度
Flutter 无法得到一个具体尺寸,就可能报错。
十四、完整运行示例:观察 build 行为
import 'package:flutter/material.dart';
void main() {
runApp(const BaseWidgetDemoApp());
}
class BaseWidgetDemoApp extends StatelessWidget {
const BaseWidgetDemoApp({super.key});
@override
Widget build(BuildContext context) {
debugPrint('BaseWidgetDemoApp build');
return const MaterialApp(
debugShowCheckedModeBanner: false,
home: BuildDemoPage(),
);
}
}
class BuildDemoPage extends StatefulWidget {
const BuildDemoPage({super.key});
@override
State<BuildDemoPage> createState() => _BuildDemoPageState();
}
class _BuildDemoPageState extends State<BuildDemoPage> {
int count = 0;
@override
Widget build(BuildContext context) {
debugPrint('BuildDemoPage build');
return Scaffold(
appBar: AppBar(
title: const Text('Base Widgets'),
),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Count: $count',
style: Theme.of(context).textTheme.headlineMedium,
),
const SizedBox(height: 16),
const StaticMessage(),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
setState(() {
count++;
});
},
child: const Text('Increase'),
),
],
),
),
);
}
}
class StaticMessage extends StatelessWidget {
const StaticMessage({super.key});
@override
Widget build(BuildContext context) {
debugPrint('StaticMessage build');
return const Text(
'I am a const child widget.',
);
}
}
首次打开页面
控制台大致输出:
BaseWidgetDemoApp build
BuildDemoPage build
StaticMessage build
点击 Increase
主要需要更新的是:
BuildDemoPage build
MaterialApp 没有重新 build。
StaticMessage 使用了 const,并且父组件可以继续返回相同实例,因此 Flutter 可以跳过大量无意义的更新工作。
十五、本章常见误区
误区一:build 执行就是整个页面重新绘制
错误。
build 只是重新生成 Widget 配置。是否需要重新布局和绘制,由后续 Element 和 RenderObject 的更新结果决定。
误区二:StatelessWidget 的 build 只执行一次
错误。
父组件更新、InheritedWidget 更新或组件重新插入时,都可能再次执行。
误区三:StatefulWidget 本身是可变的
错误。
StatefulWidget 本身仍然不可变,真正的可变数据位于 State 中。
误区四:Widget 层级越少,性能一定越好
错误。
清晰的小 Widget 通常是 Flutter 推荐的组合方式。真正需要关注的是昂贵布局、绘制、图片、列表和过大的更新范围。
误区五:子组件设置多大就一定显示多大
错误。
父组件提供的 constraints 决定子组件可以选择的尺寸范围。
误区六:build 中可以随意创建 Controller
错误。
以下对象通常需要跨多次 build 保存:
TextEditingController
ScrollController
PageController
AnimationController
FocusNode
TabController
它们一般应该在 State 中创建,并在 dispose() 中释放。
十六、本章核心总结
- Widget 是不可变的 UI 配置,不是最终显示实体。
- Element 表示 Widget 在组件树中的具体位置。
- RenderObject 负责布局、绘制和点击测试。
- Widget 可以频繁创建,Element 和 RenderObject 会尽量复用。
- rebuild 不等于整个页面重新布局和重新绘制。
- build 应快速、可重复执行并避免副作用。
- StatelessWidget 也可能多次 build。
- StatefulWidget 自身仍然不可变,可变数据保存在 State 中。
const可以帮助 Flutter 跳过部分无意义的更新工作。- Widget 层级深不一定代表性能差。
- 应根据职责、生命周期和更新范围拆分 Widget。
- Flutter 布局的核心是:父组件传约束,子组件返回尺寸,父组件决定位置。