第十章 Flutter 核心:Widget、State 与生命周期

2745 字
14 分钟
第十章 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.isReadyws.isAuthenticated 变化时,
context.watch 会触发 build 重新执行——页面自动切换。

const 构造函数的意义#

const AuthWrapper({super.key}); // ← const
// 使用时加 const
home: 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#

@override
State<ChatScreen> createState() => _ChatScreenState();

框架调用,你实现。每个 StatefulWidget 实例调用一次。

initState() —— 初始化(最重要)#

@override
void initState() {
super.initState(); // ← 必须调用 super
// 在这里做初始化工作:
// - 订阅 Stream
// - 添加监听器
// - 初始化控制器
}

⚠️ initState不能使用 context.read<T>() / context.watch<T>()——
因为此时 Widget 还没有完全挂载到树中。
解决方案:用 addPostFrameCallback 延迟到第一帧之后。

build() —— 构建 UI#

@override
Widget build(BuildContext context) {
// 在这里返回 Widget 树
// ⚠️ 此方法可能被频繁调用,不要做耗时操作!
return Container(/* ... */);
}

didUpdateWidget() —— 父 Widget 重建时#

@override
void didUpdateWidget(covariant ChatScreen oldWidget) {
super.didUpdateWidget(oldWidget);
// 父 Widget 传入的参数变化时调用
// 可以对比 oldWidget 和 widget 的差异
}

dispose() —— 销毁(必须清理资源)#

@override
void 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 件事):

  1. streamSubscription?.cancel()
  2. changeNotifier.removeListener(callback)
  3. textEditingController.dispose()
  4. scrollController.dispose()
  5. focusNode.dispose()
  6. timer?.cancel()
  7. 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
@override
Widget build(BuildContext context) {
setState(() { _x = 1; }); // 无限循环!
return Text('$_x');
}
// ❌ 错误:在 dispose 后调用 setState
@override
void dispose() {
super.dispose();
setState(() {}); // 崩溃!State 已经没了
}

mounted 检查#

// ✅ 异步回调中必须检查 mounted
Future<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;
// ...
};
// _handleAuthRequest
void _handleAuthRequest(Map<String, dynamic> request) {
if (!mounted) return; // ← Stream 回调中检查
showDialog(/* ... */);
}

10.8 🔑 WidgetsBinding.instance.addPostFrameCallback#

initState 中不能直接使用 context.read<T>() 等依赖于 InheritedWidget 的方法。解决方案是把代码延迟到第一帧之后:

@override
void 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"匹配
// 典型场景:列表项重排序时需要 key
ListView(
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🔑
跨组件访问指针/IDGlobalKey🔑

🎯 关键要点#

  1. 一切皆 Widget——Flutter 用嵌套 Widget 树描述 UI,声明式的。
  2. StatefulWidget 是双类模式——Widget 不可变(配置),State 可变(数据+构建)。
  3. initState + dispose 必须配对——尤其是 StreamSubscription、Timer、Controller 的创建和销毁。
  4. dispose 清单要牢记——遗漏任何一项都会导致内存泄漏。
  5. setState 后必须检查 mounted——异步回调中 Widget 可能已被 dispose。
  6. addPostFrameCallback 是 initState 中使用 Provider 的标准方案。
  7. const Widget 跳过重建——是 Flutter 性能优化的基本功。

📖 下一章:第 11 章 Flutter 布局与常用 Widget —— Row/Column/ListView/Scaffold 等核心布局组件。

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

第十章 Flutter 核心:Widget、State 与生命周期
https://firefly-7a0.pages.dev/posts/learn_dart/10_flutter_widgets/
作者
lonelystar
发布于
2026-03-24
许可协议
CC BY-NC-SA 4.0
最后更新于 2026-03-24,距今已过 36 天

部分内容可能已过时

评论区

Profile Image of the Author
LonelyStar
Hello, I'm LonelyStar.
公告
欢迎来到我的博客!
音乐
封面

音乐

暂未播放

0:00 0:00
暂无歌词
分类
标签
站点统计
文章
119
分类
11
标签
346
总字数
226,548
运行时长
0
最后活动
0 天前

目录