StatefulWidget 与 setState

StatefulWidget 生命周期与 build 触发时机

StatelessWidget 没有记忆,StatefulWidget 才有。判断标准很简单:能用 StatelessWidget 就不要用 StatefulWidget——状态一旦引入,你就多了生命周期、资源释放、重建范围三类负担。本章讲清 State 的生命周期、setState 的正确用法,以及状态该放在哪一层。

为什么需要 State

场景是否需要 State说明
纯展示文本、图标不需要StatelessWidget
计数器、开关、展开收起需要局部 UI 状态
表单输入内容需要也可由 TextEditingController 独立管理
服务端数据列表需要但要考虑提到上层或用 Provider

State 的生命周期

createState → initState → didChangeDependencies → build ⇄ didUpdateWidget → dispose
方法调用时机该做什么不该做什么
initState挂载时只调用一次建 Controller、发首屏请求contextInheritedWidget、调 setState
didChangeDependencies依赖的 InheritedWidget 变化响应 MediaQueryTheme放重逻辑导致频繁触发
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抛异常或无限重建移到事件回调里
异步后直接 setStatesetState() 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 内存泄漏忘记 disposeflutter analyze 提示与 DevTools Memory
flutter analyze     # 静态检查未使用字段、未 dispose 的提示
flutter test        # 用 widget test 验证计数、开关等交互

小结:状态能下沉就下沉、能用 StatelessWidget 就别上 StatefulWidgetinitState 建资源、dispose 释放资源、build 只做渲染;setState 只在事件回调里同步调用,异步回来先判断 mounted;跨层共享时再做状态提升。

笔记加载中…