第十章 Flutter 核心:Widget、State 与生命周期
第十章 Flutter 核心:Widget、State 与生命周期
目标:理解 Flutter 的 Widget 系统和 State 生命周期。Flutter 的 UI 构建方式和传统 GUI 框架(Qt、MFC)差异极大——一切皆 Widget,声明式 UI,不可变 Widget + 可变 State。
10.1 🔑 Flutter 的核心理念——“一切皆 Widget”
在 Flutter 中,所有 UI 元素都是 Widget:
Flutter UI 构成:
一个按钮?Widget。 一段文字?Widget。 一个间距?Widget。 一个圆角?Widget。 整个页面?Widget。 整个应用?Widget。声明式 UI vs 命令式 UI
// ❌ 命令式(C++ Qt / Android View)// "告诉框架怎么做"button.setText("Hello");button.setColor(Colors.blue);container.addChild(button);
// ✅ 声明式(Flutter)// "告诉框架想要什么样子"Container( color: Colors.blue, child: Text('Hello'),)💡 C++ 对比(Qt):
auto* button = new QPushButton("Hello");button->setStyleSheet("background-color: blue");layout->addWidget(button);Flutter 不需要”创建对象→设置属性→添加到容器”这三步,
而是直接描述 UI 树的样子,框架负责渲染。
Widget 树
MaterialApp( // ← 根 Widget home: Scaffold( // ← 页面骨架 appBar: AppBar( // ← 顶栏 title: Text('Lumi Hub'), ), body: Column( // ← 垂直布局 children: [ Text('Hello'), // ← 文本 ElevatedButton( // ← 按钮 onPressed: () {}, child: Text('Click'), ), ], ), ),)💡 Flutter 的 UI 就是一棵 Widget 嵌套树——通过组合小 Widget 构建复杂界面。
10.2 🔑 三棵树——Flutter 的渲染架构
Flutter 内部维护三棵树:
Widget 树 Element 树 RenderObject 树(描述,轻量) (生命周期管理) (布局和绘制,重量)
MaterialApp MaterialAppElement ←不直接产出 RenderObject └─ Scaffold ScaffoldElement RenderDecoratedBox └─ Column ColumnElement RenderFlex ├─ Text TextElement RenderParagraph └─ Button ButtonElement RenderSemanticsAnnotation| 树 | 职责 | 特点 |
|---|---|---|
| Widget 树 | 描述 UI 长什么样 | 不可变(immutable),每次 setState 重建 |
| Element 树 | 管理 Widget 与 RenderObject 的对应关系 | 持久化的,负责 diff 比较 |
| RenderObject 树 | 实际的布局计算和绘制 | 最重量级,只在必要时更新 |
💡 你写的代码 = Widget 树。Element 和 RenderObject 由框架自动管理。
关键理解:Widget 是”配方”,RenderObject 是”菜品”。
每次 setState 相当于”改了配方”,框架只重做有变化的部分。
10.3 🔑 StatelessWidget——无状态 Widget
不需要管理内部状态的 Widget。它只是”给什么画什么”。
class Greeting extends StatelessWidget { final String name;
const Greeting({super.key, required this.name}); // ← const 构造
@override Widget build(BuildContext context) { return Text('Hello, $name!'); }}
// 使用Greeting(name: 'Lumi')🔗 Lumi-Hub 实例:AuthWrapper
class AuthWrapper extends StatelessWidget { const AuthWrapper({super.key});
@override Widget build(BuildContext context) { final bootstrap = context.watch<BootstrapService>(); final ws = context.watch<WsService>();
// 根据状态决定显示哪个页面 if (!bootstrap.isReady) { return const BootstrapScreen(); // 启动中 → 启动屏 } if (!ws.isAuthenticated) { return const AuthScreen(); // 未登录 → 登录屏 } return const ChatScreen(); // 已登录 → 聊天屏 }}💡
AuthWrapper自身没有setState,所有状态来自外部(Provider)。
当bootstrap.isReady或ws.isAuthenticated变化时,
context.watch会触发build重新执行——页面自动切换。
const 构造函数的意义
const AuthWrapper({super.key}); // ← const
// 使用时加 consthome: const AuthWrapper(), // ← 编译时常量 Widget💡
const Widget不会被重建——Flutter 检测到是同一个 const 对象,直接跳过。
这是 Flutter 性能优化的基本功。
10.4 🔑 StatefulWidget——有状态 Widget
需要管理内部可变状态的 Widget。分为两部分:Widget(不可变)+ State(可变)。
基本结构(双类模式)
// 第一部分:Widget(不可变的配置)class ChatScreen extends StatefulWidget { const ChatScreen({super.key});
@override State<ChatScreen> createState() => _ChatScreenState(); // ^^^^^^^^^^^^^^^^^ // 创建对应的 State 对象}
// 第二部分:State(可变的状态 + 构建逻辑)class _ChatScreenState extends State<ChatScreen> { // 可变状态 int _counter = 0;
// 修改状态 void _increment() { setState(() { // ← 通知框架"状态变了,请重建 UI" _counter++; }); }
// 构建 UI @override Widget build(BuildContext context) { return Text('Count: $_counter'); }}为什么分成两个类?
StatefulWidget (不可变) State (可变)┌──────────────────┐ ┌──────────────────┐│ 配置参数 │ │ 可变状态 ││ key │───创建───→ │ build() ││ 其他 final 字段 │ │ initState() ││ createState() │ │ dispose() │└──────────────────┘ └──────────────────┘ ↑ 可能被频繁重建 ↑ 持久存在💡 Widget 是不可变的描述,可以频繁重建。
State 是持久化的,保存实际数据——在 Widget 重建时会被复用。
10.5 🔑 State 的完整生命周期
┌─────────────────────────────────────────────────┐ │ State 生命周期 │ │ │ │ ① createState() │ │ ↓ │ │ ② initState() ← 初始化,只调用一次 │ │ ↓ │ │ ③ didChangeDependencies() ← InheritedWidget 变化│ │ ↓ │ │ ④ build() ← 构建 UI(可能被多次调用) │ │ ↓ │ │ ⑤ didUpdateWidget() ← 父 Widget 重建时 │ │ ↓ │ │ ④ build() ← 再次构建 │ │ ↓ │ │ ... 循环 ④⑤ ... │ │ ↓ │ │ ⑥ deactivate() ← 从 Widget 树移除 │ │ ↓ │ │ ⑦ dispose() ← 永久销毁,释放资源 │ └─────────────────────────────────────────────────┘各阶段详解
① createState() —— 创建 State
@overrideState<ChatScreen> createState() => _ChatScreenState();框架调用,你实现。每个 StatefulWidget 实例调用一次。
② initState() —— 初始化(最重要)
@overridevoid initState() { super.initState(); // ← 必须调用 super // 在这里做初始化工作: // - 订阅 Stream // - 添加监听器 // - 初始化控制器}⚠️
initState中不能使用context.read<T>()/context.watch<T>()——
因为此时 Widget 还没有完全挂载到树中。
解决方案:用addPostFrameCallback延迟到第一帧之后。
④ build() —— 构建 UI
@overrideWidget build(BuildContext context) { // 在这里返回 Widget 树 // ⚠️ 此方法可能被频繁调用,不要做耗时操作! return Container(/* ... */);}⑤ didUpdateWidget() —— 父 Widget 重建时
@overridevoid didUpdateWidget(covariant ChatScreen oldWidget) { super.didUpdateWidget(oldWidget); // 父 Widget 传入的参数变化时调用 // 可以对比 oldWidget 和 widget 的差异}⑦ dispose() —— 销毁(必须清理资源)
@overridevoid dispose() { // 在这里释放资源: // - 取消 StreamSubscription // - 移除监听器 // - 销毁控制器 super.dispose(); // ← 必须调用 super(最后调用)}10.6 🔗 Lumi-Hub 实例:完整的生命周期
_LumiAppState —— 应用根 State
class _LumiAppState extends State<LumiApp> with WindowListener, TrayListener { bool _isClosing = false; final GlobalKey<NavigatorState> _navigatorKey = GlobalKey<NavigatorState>();
// ② initState:注册平台监听器 @override void initState() { super.initState(); if (!kIsWeb && Platform.isWindows) { windowManager.addListener(this); // ← 窗口事件监听 trayManager.addListener(this); // ← 托盘事件监听 _initTray(); // ← 初始化托盘图标 } }
// ⑦ dispose:移除平台监听器 @override void dispose() { if (!kIsWeb && Platform.isWindows) { trayManager.removeListener(this); // ← 移除托盘监听 windowManager.removeListener(this); // ← 移除窗口监听 } super.dispose(); }
// ④ build:构建 MaterialApp @override Widget build(BuildContext context) { final settings = context.watch<AppSettings>(); return MaterialApp( navigatorKey: _navigatorKey, title: 'Lumi Hub', themeMode: ThemeMode.system, theme: AppTheme.light(fontFamily: settings.fontFamily), darkTheme: AppTheme.dark(fontFamily: settings.fontFamily), home: const AuthWrapper(), ); }}_ChatScreenState —— 聊天页面 State
class _ChatScreenState extends State<ChatScreen> { // 控制器(需要在 dispose 中销毁) final TextEditingController _input = TextEditingController(); final ScrollController _scroll = ScrollController(); final FocusNode _focusNode = FocusNode();
// 订阅(需要在 dispose 中取消) StreamSubscription? _authSubscription; VoidCallback? _wsListener;
// 状态字段 bool _isSelectionMode = false; bool _pendingInitialBottom = true; int _lastMessageCount = 0;
// ② initState:订阅 + 添加监听 @override void initState() { super.initState(); // ↓ 用 addPostFrameCallback 延迟到第一帧后,此时 context 可用 WidgetsBinding.instance.addPostFrameCallback((_) { final ws = context.read<WsService>(); _authSubscription = ws.authRequests.listen(_handleAuthRequest); _wsListener = () { if (!mounted) return; // ← 安全检查 // ... 处理消息列表变化 ... }; ws.addListener(_wsListener!); }); _scroll.addListener(_onScrollMaybeLoadOlder); }
// ⑦ dispose:清理一切 @override void dispose() { final ws = context.read<WsService>(); if (_wsListener != null) { ws.removeListener(_wsListener!); // ← 移除 ChangeNotifier 监听 } _authSubscription?.cancel(); // ← 取消 Stream 订阅 _input.dispose(); // ← 销毁文本控制器 _scroll.dispose(); // ← 销毁滚动控制器 _focusNode.dispose(); // ← 销毁焦点节点 super.dispose(); }}💡 dispose 清单(在 dispose 中必须做的 7 件事):
- ✅
streamSubscription?.cancel()- ✅
changeNotifier.removeListener(callback)- ✅
textEditingController.dispose()- ✅
scrollController.dispose()- ✅
focusNode.dispose()- ✅
timer?.cancel()- ✅
animationController.dispose()忘了任何一个 = 内存泄漏。
10.7 🔑 setState() —— 触发 UI 重建
基本用法
void _toggleSelectionMode() { setState(() { _isSelectionMode = !_isSelectionMode; }); // 调用后,框架会重新调用 build() 方法}setState 的规则
// ✅ 正确:在 setState 内修改状态setState(() { _counter++; _name = 'new name';});
// ✅ 也正确:先修改,再调 setState_counter++;_name = 'new name';setState(() {}); // 空的也行,只是通知框架"该重建了"
// ❌ 错误:在 build 方法中调用 setState@overrideWidget build(BuildContext context) { setState(() { _x = 1; }); // 无限循环! return Text('$_x');}
// ❌ 错误:在 dispose 后调用 setState@overridevoid dispose() { super.dispose(); setState(() {}); // 崩溃!State 已经没了}mounted 检查
// ✅ 异步回调中必须检查 mountedFuture<void> loadData() async { var data = await fetchFromServer(); if (!mounted) return; // ← Widget 可能在等待期间被 dispose 了 setState(() { _data = data; });}💡
mounted的含义:这个 State 对象是否还在 Widget 树中。
如果mounted == false,调用setState会崩溃。
任何await之后都应该检查mounted。
🔗 Lumi-Hub 实例
// chat_screen.dart —— 到处都有 mounted 检查_wsListener = () { if (!mounted) return; // ← await 回来后检查 final count = ws.messages.length; // ...};
// _handleAuthRequestvoid _handleAuthRequest(Map<String, dynamic> request) { if (!mounted) return; // ← Stream 回调中检查 showDialog(/* ... */);}10.8 🔑 WidgetsBinding.instance.addPostFrameCallback
在 initState 中不能直接使用 context.read<T>() 等依赖于 InheritedWidget 的方法。解决方案是把代码延迟到第一帧之后:
@overridevoid initState() { super.initState();
// ❌ 在 initState 中直接用 context 可能不安全 // final ws = context.read<WsService>();
// ✅ 延迟到第一帧之后 WidgetsBinding.instance.addPostFrameCallback((_) { // 此时 Widget 已经完全挂载到树中,context 可用 final ws = context.read<WsService>(); _authSubscription = ws.authRequests.listen(_handleAuthRequest); });}💡
addPostFrameCallback在当前帧的 build 阶段完成后执行。
它只执行一次(不像 Timer.periodic)。
这是 Flutter 中initState+ Provider 的标准搭配方式。
10.9 GlobalKey —— 跨组件访问
GlobalKey 可以在 Widget 树的任何位置访问特定 Widget/State:
class _LumiAppState extends State<LumiApp> { final GlobalKey<NavigatorState> _navigatorKey = GlobalKey<NavigatorState>();
@override Widget build(BuildContext context) { return MaterialApp( navigatorKey: _navigatorKey, // ← 把 key 绑定到 Navigator // ... ); }
Future<WindowCloseAction> _confirmCloseAction(AppSettings settings) async { final dialogContext = _navigatorKey.currentContext; // ^^^^^^^^^^^^^^ // 通过 GlobalKey 获取 Navigator 的 context if (dialogContext == null) return WindowCloseAction.minimize;
final result = await showDialog<WindowCloseAction>( context: dialogContext, // ← 用它来弹对话框 // ... ); }}💡 为什么需要 GlobalKey?
onWindowClose回调不是从 Widget 树触发的(是平台事件),
此时context可能不可用。通过GlobalKey可以在任何位置拿到目标 Widget 的 context。
⚠️ GlobalKey 的代价:每个 Key 必须全局唯一,框架需要维护查找表。
不要滥用——只在确实需要跨组件访问时才用。
10.10 super.key 与 Widget Key
每个 Widget 都有 key 参数
class ChatScreen extends StatefulWidget { const ChatScreen({super.key}); // ← super.key 传给父类 Widget // ...}Key 的作用
// 没有 key:框架通过"位置"匹配新旧 Widget// 有 key:框架通过"key"匹配
// 典型场景:列表项重排序时需要 keyListView( children: items.map((item) => ListTile( key: ValueKey(item.id), // ← 用 id 作为 key title: Text(item.name), ), ).toList(),)Key 的类型
| Key 类型 | 用途 |
|---|---|
ValueKey(value) | 按值匹配(最常用) |
ObjectKey(object) | 按引用匹配 |
UniqueKey() | 每次都不同(强制重建) |
GlobalKey() | 全局唯一,可跨组件访问 State |
10.11 本章小结
| 概念 | 传统 GUI (Qt/MFC) | Flutter | 重要程度 |
|---|---|---|---|
| UI 构建方式 | 命令式 | 声明式(Widget 树) | 🔑🔑🔑 |
| 渲染架构 | View 树 | Widget + Element + RenderObject 三棵树 | 🔑🔑 |
| 无状态组件 | 有 | StatelessWidget + build() | 🔑 |
| 有状态组件 | 有 | StatefulWidget + State 双类模式 | 🔑🔑🔑 |
| 初始化 | 构造函数 | initState() | 🔑🔑 |
| 资源释放 | 析构函数 | dispose()(必须手动清理) | 🔑🔑🔑 |
| 状态更新 | update() / invalidate() | setState() | 🔑🔑 |
| 安全检查 | — | mounted | 🔑🔑 |
| 延迟初始化 | — | addPostFrameCallback | 🔑 |
| 跨组件访问 | 指针/ID | GlobalKey | 🔑 |
🎯 关键要点
- 一切皆 Widget——Flutter 用嵌套 Widget 树描述 UI,声明式的。
- StatefulWidget 是双类模式——Widget 不可变(配置),State 可变(数据+构建)。
initState+dispose必须配对——尤其是 StreamSubscription、Timer、Controller 的创建和销毁。dispose清单要牢记——遗漏任何一项都会导致内存泄漏。setState后必须检查mounted——异步回调中 Widget 可能已被 dispose。addPostFrameCallback是 initState 中使用 Provider 的标准方案。const Widget跳过重建——是 Flutter 性能优化的基本功。
📖 下一章:第 11 章 Flutter 布局与常用 Widget —— Row/Column/ListView/Scaffold 等核心布局组件。
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!
部分内容可能已过时