面试备战 Flutter 19:Widget 不可变、生命周期与 Build 机制
面试备战 Flutter 19:Widget 不可变、生命周期与 Build 机制
Flutter 面试里,“Widget 为什么不可变”和“为什么 build 会频繁执行”经常连在一起问。表面是在问 API 设计,实际是在考你是否理解 Flutter 的三棵树模型:
Widget -> 不可变配置
Element -> 树中位置和生命周期
State -> StatefulWidget 的可变状态
RenderObject -> 布局、绘制、命中测试
一句话回答:
Widget 设计成 immutable,是为了让 UI 成为某一时刻状态的纯描述。状态变化时不修改旧 Widget,而是生成新 Widget,再由 Element 根据
runtimeType和key决定复用还是替换,从而让 build 可以频繁、廉价、可预测地执行。
1. Widget 到底是什么?
Widget 不是屏幕上的 View,也不是最终参与布局绘制的对象。
Widget 是一份配置:
Text描述一段文字应该怎么显示。Container描述背景、边距、约束、子节点。Scaffold描述页面骨架。StatefulWidget描述一个需要绑定 State 的配置入口。
真正持久存在的是 Element。Widget 每次 build 都可能重新创建,但 Element 会尽量复用。
可以这样理解:
Widget = 配置说明书
Element = 说明书在树上的安装位置
State = 这个位置关联的可变状态
RenderObject = 真正执行布局和绘制的对象
2. 为什么 Widget 必须不可变?
2.1 让 UI 成为状态的快照
不可变 Widget 表示:这个对象一旦创建,就只代表某一刻的 UI 配置。
当状态变化时,Flutter 不会在原 Widget 上改字段,而是重新调用 build,生成一批新 Widget:
old state -> old widgets
new state -> new widgets
这样 UI 更新就变成一个简单的转换:
State -> Widget description
这非常适合声明式 UI。
2.2 降低 diff 成本
Flutter 更新 Element 时,最核心的匹配规则是:
oldWidget.runtimeType == newWidget.runtimeType
&& oldWidget.key == newWidget.key
如果匹配成功,Element 复用,只更新配置;如果不匹配,旧 Element 被卸载,新 Element 被挂载。
Widget 不可变后,框架不用追踪一个 Widget 内部哪些字段被改过,只需要比较新旧 Widget 配置即可。
2.3 避免共享可变对象带来的混乱
如果 Widget 可变,就会出现很多难以判断的问题:
- 一个 Widget 被多个地方持有,谁改了它?
- Element 正在 build 时,Widget 字段被另一个逻辑改了怎么办?
- 新旧配置边界在哪里?
- 热重载后状态和配置如何安全恢复?
不可变对象天然没有“半更新”状态,框架和开发者的心智都会简单很多。
2.4 让 build 变得廉价
Flutter 官方 API 文档强调:build 可能每一帧都被调用。
这只有在 Widget 足够轻、可频繁创建的前提下才成立。Widget 不持有昂贵资源、不承载真实布局绘制对象,就可以被频繁丢弃和重建。
真正昂贵的对象,如 State、Element、RenderObject,会被框架尽量复用。
3. StatefulWidget 为什么还能保持状态?
一个常见误解是:StatefulWidget 能变,所以它不是 immutable。
事实正好相反:
StatefulWidget 也是 immutable。能变的是它关联的 State。
典型流程:
父组件 rebuild
-> 创建新的 StatefulWidget 实例
-> Element 判断 type + key 一致
-> 复用原来的 State
-> State.widget 指向新的 Widget 配置
-> 调用 didUpdateWidget
-> 再调用 build
示例:
class UserHeader extends StatefulWidget {
const UserHeader({
super.key,
required this.userId,
});
final String userId;
State<UserHeader> createState() => _UserHeaderState();
}
userId 变化时,不是原来的 UserHeader 被修改,而是父节点创建了一个新的 UserHeader(userId: newId)。如果 key 和类型匹配,_UserHeaderState 会保留下来。
这就是为什么在 State 里访问最新配置要用 widget.userId。
4. Widget、Element、State、RenderObject 生命周期
面试里讲 Flutter 生命周期,不能只背 initState -> build -> dispose。更完整的回答应该分层。
| 层级 | 职责 | 生命周期特点 |
|---|---|---|
| Widget | 不可变 UI 配置 | 随 build 频繁创建和丢弃 |
| Element | Widget 在树中的位置 | mount、update、rebuild、deactivate、unmount |
| State | StatefulWidget 可变状态 | initState、didChangeDependencies、didUpdateWidget、build、dispose |
| RenderObject | 布局、绘制、命中测试 | attach、layout、paint、detach |
5. State 生命周期逐个讲
createState
框架第一次挂载 StatefulWidget 时调用,用来创建 State。
不要在这里读 context,通常也不要做业务初始化。
initState
State 插入树后调用一次。
适合:
- 创建
AnimationController。 - 创建
TextEditingController。 - 订阅 Stream。
- 启动只依赖 widget 初始参数的任务。
示例:
void initState() {
super.initState();
controller = AnimationController(vsync: this);
}
注意:initState 里可以读 widget,但不要用 context.dependOnInheritedWidgetOfExactType 订阅依赖。需要依赖 InheritedWidget 时放到 didChangeDependencies。
didChangeDependencies
initState 后立即调用一次;之后如果依赖的 InheritedWidget 变化,还会再次调用。
适合:
- 依赖
Theme.of(context)、Localizations.of(context)、MediaQuery.of(context)的初始化。 - 依赖 Provider / InheritedWidget 的订阅型逻辑。
build
把当前状态描述成 Widget。
build 必须满足:
- 快。
- 可重复。
- 无副作用。
- 不创建需要手动释放的长期资源。
错误示例:
Widget build(BuildContext context) {
fetchUser(); // 错:每次 build 都可能触发请求
return const SizedBox();
}
didUpdateWidget
父组件 rebuild 后,如果新旧 Widget 类型和 key 一致,Element 会复用同一个 State,并调用 didUpdateWidget。
适合处理“配置参数变化”:
void didUpdateWidget(covariant UserHeader oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.userId != widget.userId) {
reloadUser(widget.userId);
}
}
deactivate
State 从树上移除时调用。它不一定代表销毁,因为 GlobalKey reparent 时可能先 deactivate,再重新插入树。
通常不要在这里释放最终资源,最终释放放到 dispose。
dispose
State 永久销毁时调用。
必须释放:
- Controller。
- FocusNode。
- StreamSubscription。
- Timer。
- AnimationController。
void dispose() {
subscription.cancel();
controller.dispose();
super.dispose();
}
6. 为什么 build 会频繁执行?
build 频繁执行是 Flutter 的设计预期,不是异常。
常见触发来源:
setState。- 父组件 rebuild。
InheritedWidget变化,比如 Theme、MediaQuery、Localizations。- Provider/Riverpod 等状态变化。
- 动画每帧 tick。
didUpdateWidget后框架调度 build。- 热重载。
官方文档对 build 的要求可以概括为:
假设 build 随时可能被调用,包括每一帧。
所以不能把 build 当成“只执行一次的初始化函数”。
7. build 频繁执行为什么还能快?
因为 Flutter 区分了“配置重建”和“真实对象复用”。
Widget rebuild 很频繁
Element 尽量复用
State 尽量复用
RenderObject 尽量复用
Layer 在合适情况下复用
举例:
setState(() {
count++;
});
它不是把整棵 App 树全部销毁重建,而是:
- 当前 Element 标记 dirty。
- 下一帧 BuildOwner 统一处理 dirty elements。
- 调用对应 build。
- 新 Widget 和旧 Element 匹配。
- 能复用的 Element/RenderObject 继续复用。
8. build 里不能做什么?
不要做 CPU 重计算
final parsed = jsonDecode(hugeJson);
大 JSON 解析、加解密、图片处理应该放到 compute 或 Isolate.run。
不要创建长期对象
final controller = TextEditingController();
这会导致每次 build 创建新对象,而且旧对象可能没有释放。
不要发网络请求
repository.fetch();
父组件 rebuild、键盘弹出、主题变化都可能导致重复请求。
不要注册监听
stream.listen(...);
这会造成重复订阅和内存泄漏。
9. 面试怎么回答?
可以这样答:
Widget 是不可变的 UI 配置,不是真正的 View。Flutter 把 Widget 设计成 immutable,是为了让 UI 成为某一刻状态的纯描述。状态变化后,框架重新 build 新 Widget,再由 Element 根据
runtimeType和key判断复用还是替换。这样 build 可以频繁执行,但真正昂贵的 State、Element、RenderObject 会被复用。StatefulWidget 自己也不可变,变化的是 State。父组件传入新参数后,如果类型和 key 一致,原 State 会保留,
State.widget指向新配置,并触发didUpdateWidget。build 可能每帧执行,所以必须无副作用、轻量、可重复。初始化放
initState,依赖 InheritedWidget 的逻辑放didChangeDependencies,参数变化处理放didUpdateWidget,资源释放放dispose,CPU 重活放 Isolate。
参考资料
- Flutter Architectural overview: https://docs.flutter.dev/resources/architectural-overview
- Flutter Inside Flutter: https://docs.flutter.dev/resources/inside-flutter
- StatefulWidget API: https://api.flutter.dev/flutter/widgets/StatefulWidget-class.html
- State API: https://api.flutter.dev/flutter/widgets/State-class.html
- StatelessWidget.build API: https://api.flutter.dev/flutter/widgets/StatelessWidget/build.html
- Widget API: https://api.flutter.dev/flutter/widgets/Widget-class.html