这是一篇结合“教程型 + 概念图解 + 源码导读”的文章。你将先获得一张可记忆的总览图,然后用最小示例串联各回调的真实触发时机,最后从源码视角验证直觉并掌握边界与最佳实践。
读者与目标#
- 面向对象:初、中级
- 目标:搞清楚哪些事该放在哪个生命周期回调里,并理解其底层触发链路
- 覆盖范围:Widget/Element/State、Route(页面路由)、App(前后台)三个层级
生命周期一图总览#
把三棵树记住:Widget(描述)→ Element(实例,连接树形)→ RenderObject(布局/绘制)。生命周期主要发生在 State
(对应 StatefulWidget 的可变部分)与 Element
上。
时序主线(StatefulWidget):
reateState→ initState→ didChangeDependencies // 依赖(如 InheritedWidget)建立后触发→ build // 可多次→ didUpdateWidget? // 父 Widget 配置更新时→ reassemble? // 热重载→ deactivate // 临时移出树(如路由切换)→ dispose // 永久移出 / 销毁
辅助线:
- setState → 标记当前
Element
脏 → 下个 frame 触发build
- Route 页面可见性:
RouteAware.didPush/didPop/didPopNext/didPushNext
- App 前后台:
WidgetsBindingObserver.didChangeAppLifecycleState
(resumed/paused/inactive/detached)
[!tip] 口诀
- “初(init)定依(deps)后构(build),父改(update)再构;退场(deactivate)别忘记,谢幕(dispose)要清场。”
该放哪里做什么(对照表)#
时机 | 回调 | 典型用途 | 注意事项 |
---|---|---|---|
创建 | initState | 一次性初始化:控制器、订阅、启动轻量异步 | 不要在此直接 async/await 阻塞构建;需要 context 的依赖请移到 didChangeDependencies 或 postFrameCallback |
依赖建立 | didChangeDependencies | 读取依赖(如 InheritedWidget / Provider),或 RouteObserver 订阅 | 可能被多次调用(依赖变化);避免重复订阅(用标志位) |
构建 | build | 纯函数式描述 UI | 避免重型同步任务与副作用 |
父配置更新 | didUpdateWidget | 对比 oldWidget 调整内部状态 | 使用 widget.xxx 与 oldWidget.xxx 比较 |
热重载 | reassemble | 开发期修正调试状态 | 仅开发期有效 |
临时移出树 | deactivate | 临时分离时清理与迁移 | 可能会重新插回(紧跟着又 build) |
销毁 | dispose | 成对释放(Controller、Listener、Timer、Stream 等) | 一切异步回调需判断 mounted ,避免“僵尸 setState” |
最小可运行示例#
下面示例把各回调的触发时机打印到控制台,同时演示 Route 与 App 生命周期。
import 'package:flutter/material.dart';
final RouteObserver<PageRoute<dynamic>> routeObserver = RouteObserver<PageRoute<dynamic>>();
void main() { runApp(MaterialApp( home: const PageA(), navigatorObservers: [routeObserver], ));}
class PageA extends StatelessWidget { const PageA({super.key});
@override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Page A')), body: Center( child: ElevatedButton( child: const Text('Go to Page B'), onPressed: () => Navigator.of(context).push( MaterialPageRoute(builder: (_) => const PageB()), ), ), ), ); }}
class PageB extends StatefulWidget { const PageB({super.key});
@override State<PageB> createState() => _PageBState();}
class _PageBState extends State<PageB> with WidgetsBindingObserver, RouteAware { bool _subscribed = false;
@override void initState() { super.initState(); debugPrint('initState'); WidgetsBinding.instance.addObserver(this);
// 轻量异步或预取,不阻塞首帧 Future.microtask(() => debugPrint('microtask after initState'));
// 若需要获取尺寸/布局信息,用 frame 末尾回调 WidgetsBinding.instance.addPostFrameCallback((_) { debugPrint('postFrameCallback: can read size/layout'); }); }
@override void didChangeDependencies() { super.didChangeDependencies(); debugPrint('didChangeDependencies');
// 在这里订阅路由可见性(需要 context 才能拿到当前 Route) if (!_subscribed) { final route = ModalRoute.of(context); if (route is PageRoute) { routeObserver.subscribe(this, route); _subscribed = true; } } }
@override void didUpdateWidget(covariant PageB oldWidget) { super.didUpdateWidget(oldWidget); debugPrint('didUpdateWidget: ${oldWidget != widget}'); }
@override Widget build(BuildContext context) { debugPrint('build'); return Scaffold( appBar: AppBar(title: const Text('Page B')), body: Center( child: ElevatedButton( child: const Text('setState'), onPressed: () => setState(() { debugPrint('setState -> schedule rebuild'); }), ), ), ); }
// RouteAware @override void didPush() => debugPrint('RouteAware.didPush'); @override void didPop() => debugPrint('RouteAware.didPop'); @override void didPushNext() => debugPrint('RouteAware.didPushNext'); @override void didPopNext() => debugPrint('RouteAware.didPopNext');
// App 生命周期 @override void didChangeAppLifecycleState(AppLifecycleState state) { debugPrint('AppLifecycleState: $state'); // resumed/paused/inactive/detached }
@override void deactivate() { super.deactivate(); debugPrint('deactivate'); }
@override void dispose() { debugPrint('dispose'); if (_subscribed) routeObserver.unsubscribe(this); WidgetsBinding.instance.removeObserver(this); super.dispose(); }}
运行步骤建议:
- 启动进入 PageA,点击进入 PageB:观察
didPush → build
。 - 点击按钮触发
setState
:观察重建不走initState
。 - 返回 PageA:观察
didPop → deactivate → dispose
。 - 切后台/回前台:观察
AppLifecycleState
变化。
[!warning] 常见报错
- “setState() called after dispose()”:异步返回时页面已销毁,回调里要先判
mounted
。
if (!mounted) return;setState(() { /* update */ });
三个高频实战场景#
-
首屏数据预取(推荐位于
initState
+postFrameCallback
)- 读依赖(如 Provider)→ 放
didChangeDependencies
- 需要尺寸或
Overlay
/ScaffoldMessenger
的 UI 操作 → 放postFrameCallback
- 读依赖(如 Provider)→ 放
-
路由可见性驱动的统计/播放控制
- 用
RouteObserver + RouteAware
,在didChangeDependencies
订阅,在dispose
取消 - 播放暂停/恢复可放在
didPushNext
/didPopNext
- 用
-
App 前后台切换的数据持久/资源管理
WidgetsBindingObserver.didChangeAppLifecycleState
paused
持久化,resumed
恢复资源;注意与dispose
区分层级
源码导读#
-
初次挂载
StatefulElement.mount()
→state = widget.createState()
→state.initState()
_firstBuild()
→BuildOwner.buildScope(element)
→element.performRebuild()
→ 调用State.build()
-
依赖变化
- 当
InheritedElement
标记依赖者时,相关Element
在下一帧performRebuild()
前会触发didChangeDependencies()
,随后进入build()
- 当
-
父配置更新
StatefulElement.update(newWidget)
→state.didUpdateWidget(oldWidget)
→markNeedsBuild()
→ 下帧build()
-
销毁
deactivate()
在从树上移除时调用(可能会再插回)- 确认不再复用时进入
unmount()
,触发state.dispose()
-
帧调度与重建
setState()
→element.markNeedsBuild()
→SchedulerBinding.scheduleFrame()
- 每帧
handleBeginFrame
→buildOwner.buildScope
统一批处理重建
[!tip] 观察点
- 重建是“批处理+按帧”的,不必担心一次多次
setState
会立即多次重建,框架会合并到同一帧。
易错点与最佳实践#
- 在
dispose
中释放一切成对资源:Controller/FocusNode/AnimationController/StreamSubscription/Timer
- 不在
build
里发起网络/写文件/订阅;副作用放initState
或didChangeDependencies
- 需要
context
的弹框/布局信息:放postFrameCallback
- 订阅类操作用布尔标志防重复(
didChangeDependencies
可能重复触发) - 异步回调中先判
mounted
,再setState
- 父更新引发的内部状态调整放
didUpdateWidget
- 热重载相关逻辑放
reassemble
(仅开发期) - 性能:
build
要小而快;把重活放到RenderObject
/CustomPainter
或上层数据转换
记忆小抄#
- “初定依后构,多次构;父改再构;退场别忘记,谢幕要清场。”
- 层级:Widget(描述)→ Element(实例)→ RenderObject(渲染)
- 三监听:RouteAware 看“页面可见”,WidgetsBindingObserver 看“前后台”,postFrameCallback 看“这一帧末”
参考实践片段#
- 在
didChangeDependencies
中安全地拿到当前Route
:
final route = ModalRoute.of(context);if (route is PageRoute) { routeObserver.subscribe(this, route);}
- 在帧结束读布局信息:
WidgetsBinding.instance.addPostFrameCallback((_) { final size = context.size; // or use LayoutBuilder/Measure});
- 对比新旧配置:
@overridevoid didUpdateWidget(covariant MyWidget oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.config != widget.config) { // update internal state }}
- 以上内容已按你的博客 `postSchema`(如 `title/pubDate/toc/ogImage` 等)生成,可直接保存为新文章文件。- 若需要插入配图或流程图,我可以补充 PNG/ASCII 图或提供 Mermaid 版本。