Widget 生命周期:StatefulWidget 的 8 个阶段
为什么要理解生命周期?
错误地在生命周期方法中执行操作是 Flutter 开发中常见的 bug 来源:在错误的阶段访问 context、重复初始化、内存泄漏(忘记释放资源)。理解生命周期,才能正确安置代码。
StatelessWidget 的生命周期
StatelessWidget 极简:只有 build。每当依赖变化(父 rebuild 或 InheritedWidget 更新),就重新调用 build,别无其他。
StatefulWidget 的 8 个阶段
createState()
↓
initState()
↓
didChangeDependencies()
↓
build() ← ─────────────────────────┐
↓ │
(mounted) setState() / 依赖变化
│ │
├─ didUpdateWidget() ──────────┘ (父 Widget 重建时)
│
↓
deactivate()
↓
dispose()
↓
(unmounted)
1. createState()
在 StatefulWidget.createState() 中创建 State 对象。每个 StatefulWidget 对应一个 State,一一对应。
class MyWidget extends StatefulWidget {
@override
State<MyWidget> createState() => _MyWidgetState();
}
2. initState()
State 对象被创建后立即调用,只调用一次。这是初始化的标准位置:
class _MyWidgetState extends State<MyWidget> {
late final StreamSubscription _subscription;
late final TextEditingController _controller;
@override
void initState() {
super.initState(); // 必须先调用 super
_controller = TextEditingController();
_subscription = someStream.listen(_onData);
// ❌ 不能在这里调用 context.read() 或 InheritedWidget
// ✅ 可以访问 widget 属性(widget.someProperty)
}
}
注意:initState 阶段 context 尚未完全建立,不能调用任何依赖 context 的 Provider/InheritedWidget API。
3. didChangeDependencies()
在 initState 之后、首次 build 之前至少调用一次。当 State 依赖的 InheritedWidget 变化时也会再次调用。
@override
void didChangeDependencies() {
super.didChangeDependencies();
// ✅ 这里可以安全访问 context
final theme = Theme.of(context);
final locale = Localizations.localeOf(context);
}
在这里可以做「依赖 context 的初始化」:
@override
void didChangeDependencies() {
super.didChangeDependencies();
// 例如:首次加载数据(但要防止重复调用)
if (!_initialized) {
_initialized = true;
context.read<MyBloc>().add(LoadDataEvent());
}
}
4. build()
每次 Widget 需要重新渲染时调用。在以下情况触发:
setState()调用- 父 Widget rebuild
- 依赖的
InheritedWidget变化
build 必须是纯函数(只根据当前 state 和 widget 生成 UI,无副作用)。
@override
Widget build(BuildContext context) {
// ❌ 避免在这里做耗时操作、修改状态、触发副作用
return Text(widget.title);
}
5. didUpdateWidget()
当父 Widget rebuild 导致同类型但不同实例的 Widget 传来时调用。此时 widget 属性已是新 Widget:
@override
void didUpdateWidget(MyWidget oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.url != widget.url) {
// URL 变了,重新加载数据
_loadData(widget.url);
}
if (oldWidget.controller != widget.controller) {
// 控制器变了,重新订阅
_subscription?.cancel();
_subscription = widget.controller.stream.listen(_onData);
}
}
比较新旧 Widget 属性是 didUpdateWidget 的典型用途。
6. setState()
触发 build 的方法。实际上是标记当前 Element 为 dirty,等待下一帧重建:
void _increment() {
setState(() {
_counter++;
// ✅ 修改状态的代码应放在 setState 回调内(清晰表达意图)
// 实际上放在外面也行,但不推荐
});
}
setState 的常见错误:
// ❌ 在 dispose 后调用 setState(会报错)
@override
void dispose() {
_timer.cancel();
super.dispose();
}
// 如果 timer 回调在 dispose 后触发
void _onTimer() {
if (mounted) { // ✅ 用 mounted 判断
setState(() { ... });
}
}
7. deactivate()
Element 从树中暂时移除时调用(例如被 Navigator.push 压入后台)。State 可能在之后被重新插回树中。
@override
void deactivate() {
super.deactivate();
// 暂停一些操作(如视频播放)
}
大多数情况下不需要覆写 deactivate,dispose 更常用。
8. dispose()
State 被永久销毁时调用。必须在这里释放所有资源:
@override
void dispose() {
_controller.dispose(); // 释放 TextEditingController
_subscription.cancel(); // 取消 Stream 订阅
_animationController.dispose(); // 释放动画控制器
_focusNode.dispose(); // 释放 FocusNode
super.dispose(); // 必须最后调用 super
}
忘记 dispose 是内存泄漏的首要原因,特别是 AnimationController、StreamSubscription、TextEditingController。
mounted 属性
State.mounted 表示当前 State 是否还关联着树中的 Element:
// 异步操作后更新 UI 的标准模式
Future<void> _loadData() async {
final data = await repository.fetchData();
if (!mounted) return; // 如果已销毁,不再更新 UI
setState(() {
_data = data;
});
}
生命周期完整时序图
┌─────────────────────────────────────────────────────┐
│ createState → initState → didChangeDependencies │
│ ↓ │
│ build │
│ (首次渲染) │
├─────────────────────────────────────────────────────┤
│ setState() → build(再次渲染) │
│ 父 rebuild + 同类 Widget → didUpdateWidget → build │
│ 依赖 InheritedWidget 变化 → didChangeDependencies → build │
├─────────────────────────────────────────────────────┤
│ deactivate(暂时移出树) │
│ dispose(永久销毁) │
└─────────────────────────────────────────────────────┘
核心工程问题
Q:initState 和 didChangeDependencies 的区别?
initState 只调用一次,不能访问 context(的 InheritedWidget 部分)。didChangeDependencies 调用时 context 完整,依赖变化也会重调。需要依赖 context 初始化的逻辑放 didChangeDependencies(加 _initialized 守卫避免重复执行)。
Q:为什么 dispose 要调用 super.dispose()?
State.dispose 的基类实现会标记 State 为 unmounted(设置内部 _debugLifecycleState),并断开与 Element 的关联。如果不调 super,可能导致内存泄漏和调试工具无法正常工作。
Q:GlobalKey 能保留 State?
是的。GlobalKey 让 Element(和 State)跟 Widget 的树位置解耦。即使 Widget 在树中移动,只要 Key 相同,State 就继续存活(不会 dispose → createState 重来)。这在如 PageView 内跨页持久化复杂 State 时非常有用。