StatefulWidget 与 setState

StatelessWidget 没有记忆,StatefulWidget 才有。判断标准很简单:能用 StatelessWidget 就不要用 StatefulWidget——状态一旦引入,你就多了生命周期、资源释放、重建范围三类负担。本章讲清 State 的生命周期、setState 的正确用法,以及状态该放在哪一层。
为什么需要 State
| 场景 | 是否需要 State | 说明 |
|---|
| 纯展示文本、图标 | 不需要 | 用 StatelessWidget |
| 计数器、开关、展开收起 | 需要 | 局部 UI 状态 |
| 表单输入内容 | 需要 | 也可由 TextEditingController 独立管理 |
| 服务端数据列表 | 需要 | 但要考虑提到上层或用 Provider |
State 的生命周期
createState → initState → didChangeDependencies → build ⇄ didUpdateWidget → dispose
| 方法 | 调用时机 | 该做什么 | 不该做什么 |
|---|
initState | 挂载时只调用一次 | 建 Controller、发首屏请求 | 用 context 取 InheritedWidget、调 setState |
didChangeDependencies | 依赖的 InheritedWidget 变化 | 响应 MediaQuery、Theme | 放重逻辑导致频繁触发 |
build | 每次需要渲染时 | 纯粹的界面构建 | 发请求、建 Controller、改状态 |
didUpdateWidget | 父级传入的配置变了 | 对比新旧参数做增量更新 | 无脑重新请求 |
dispose | 永久销毁时 | 释放 Controller、取消订阅 | 调 setState |
class SearchField extends StatefulWidget {
const SearchField({super.key, this.initialText = ''});
final String initialText;
@override
State<SearchField> createState() => _SearchFieldState();
}
class _SearchFieldState extends State<SearchField> {
late final TextEditingController _controller;
final _focusNode = FocusNode();
@override
void initState() {
super.initState();
_controller = TextEditingController(text: widget.initialText); // 此时可访问 widget
}
@override
void didUpdateWidget(covariant SearchField oldWidget) {
super.didUpdateWidget(oldWidget);
// 外部传入的初始值变了才同步,避免覆盖用户输入
if (oldWidget.initialText != widget.initialText) _controller.text = widget.initialText;
}
@override
void dispose() {
_controller.dispose(); // 不释放会内存泄漏
_focusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) => TextField(
controller: _controller,
focusNode: _focusNode,
decoration: const InputDecoration(hintText: '输入关键字', prefixIcon: Icon(Icons.search)),
);
}
setState 的正确用法
class _CounterState extends State<Counter> {
int _count = 0;
bool _loading = false;
// 正确:改数据与通知刷新放在一起,同步完成
void _increment() => setState(() => _count++);
// 正确:异步回来后先判断 mounted,再 setState
Future<void> _loadRemote() async {
setState(() => _loading = true);
final value = await Future<int>.delayed(const Duration(seconds: 1), () => 42);
if (!mounted) return; // 页面已销毁,直接返回
setState(() {
_count = value;
_loading = false;
});
}
@override
Widget build(BuildContext context) => Column(
children: [
Text('计数:$_count'),
FilledButton(onPressed: _loading ? null : _loadRemote, child: const Text('加载')),
],
);
}
| 错误写法 | 后果 | 正确做法 |
|---|
在 build 里调 setState | 抛异常或无限重建 | 移到事件回调里 |
异步后直接 setState | 抛 setState() called after dispose() | 先 if (!mounted) return; |
只改字段不调 setState | 界面不刷新 | 把赋值包进 setState |
只改列表元素(list[0] = x) | 引用未变,可能不刷新 | 新建集合后整体赋值 |
状态的三个层次
| 状态种类 | 放哪里 | 例子 | 刷新范围 |
|---|
| 局部 UI 状态 | 当前 State | 展开/收起、Tab 下标 | 只有本组件 |
| 局部业务状态 | 页面级 State | 加载中、列表数据 | 整页 |
| 跨页面共享状态 | 祖先 State 或状态管理库 | 登录用户、购物车 | 订阅者 |
"状态提升"就是找到使用它的组件的最近公共祖先,把状态放上去,参数往下传、回调往上传:
App
└─ Page(持有 selectedId)
├─ ListView(读 selectedId,点回调通知 Page)
└─ DetailPanel(读 selectedId)
这条链超过三层就该考虑 Provider;只是兄弟间共享一层,提升到父级 State 就够了。
常见坑与调试方法
| 现象 | 原因 | 排查方式 |
|---|
setState() called after dispose() | 异步回调晚于页面销毁 | 加 mounted 判断,或用 CancelToken 取消请求 |
Looking up a deactivated widget's ancestor | 异步中用了过期 context | 提前存下 ScaffoldMessenger.of(context) |
| Controller 内存泄漏 | 忘记 dispose | 看 flutter analyze 提示与 DevTools Memory |
flutter analyze # 静态检查未使用字段、未 dispose 的提示
flutter test # 用 widget test 验证计数、开关等交互
小结:状态能下沉就下沉、能用 StatelessWidget 就别上 StatefulWidget;initState 建资源、dispose 释放资源、build 只做渲染;setState 只在事件回调里同步调用,异步回来先判断 mounted;跨层共享时再做状态提升。