Flutter 基础体系 · 第 33/80 篇。示例基于当前稳定 Flutter 与 Dart 3 语言能力;Android、iOS、桌面和 Web 差异会明确说明。

Flutter Widget 生命周期:Element、State、BuildContext 和更新顺序

Flutter 的界面不是由 Widget 对象直接“显示”出来的。一次界面更新至少涉及三类对象:

  • Widget:描述界面的不可变配置。
  • Element:把 Widget 配置挂接到树上的长期存在对象。
  • State:为 StatefulWidget 保存可变状态,并参与构建过程。
  • BuildContext:访问当前 Element 位置和上下文的接口。

如果只记住 initStatebuilddispose,很容易在异步回调、列表重排、依赖变化或父组件更新时产生错误判断。理解生命周期的关键,是先区分这几类对象的职责,再分析 Flutter 如何匹配、更新和销毁它们。


一、先建立三棵树和一次更新的基本模型

1. Widget 是不可变配置

Widget 通常只描述:

  • 类型;
  • 构造参数;
  • 子 Widget;
  • 用于匹配的 key

例如:

class UserTitle extends StatelessWidget {
  const UserTitle({
    super.key,
    required this.name,
  });

  final String name;

  @override
  Widget build(BuildContext context) {
    return Text(name);
  }
}

当父组件从 UserTitle(name: 'Alice') 构建为 UserTitle(name: 'Bob') 时,通常会产生一个新的 UserTitle 对象。旧对象不会被修改,新的对象只是下一轮构建使用的配置。

因此,下面这种写法不能用于保存会变化的数据:

class CounterLabel extends StatelessWidget {
  CounterLabel({super.key}) : count = 0;

  int count;

  @override
  Widget build(BuildContext context) {
    return Text('$count');
  }
}

StatelessWidget 实例可能在后续构建中被替换,且 Widget 的设计目标是不可变配置。可变状态应放入 State、模型对象或其他状态管理机制中。

2. Element 是 Widget 在树中的身份

Element 是 Widget 的实例化位置。它负责:

  • 保存当前 Widget 配置;
  • 保存父子关系;
  • 参与 Widget 更新和匹配;
  • 记录是否需要重新构建;
  • BuildContext 提供运行时上下文;
  • 对 StatefulWidget 保存对应的 State。

可以把 Widget 理解为“这一次想要的配置”,把 Element 理解为“树中的位置和身份”。

一次构建前后可能是:

旧 Widget: Text("A")
    ↓ 更新配置
同一个 Element
    ↓
新 Widget: Text("B")

如果 Element 被复用,它的身份保持不变,只是持有的 Widget 配置变了。

3. State 是 StatefulWidget 的可变生命周期对象

StatefulWidget 本身仍然是不可变的。可变数据保存在单独的 State 对象中:

class Counter extends StatefulWidget {
  const Counter({super.key});

  @override
  State<Counter> createState() => _CounterState();
}

class _CounterState extends State<Counter> {
  int count = 0;

  @override
  Widget build(BuildContext context) {
    return Text('$count');
  }
}

这里有三个不同对象:

Counter Widget       :当前配置,例如构造参数
_CounterState        :count 等可变状态
StatefulElement      :将 Widget 和 State 连接到树中的 Element

当父组件重新创建 Counter 时,Flutter 通常不会重新创建 _CounterState。只要新旧 Widget 能够匹配,原来的 State 会继续使用,只更新它的 widget 引用。

4. BuildContext 实际上代表 Element

BuildContext 是一个接口,常见实现是 Element。它不是 Widget,也不是 State。

@override
Widget build(BuildContext context) {
  final theme = Theme.of(context);
  return Text(
    'Hello',
    style: theme.textTheme.titleLarge,
  );
}

这个 context 表示当前 Widget 在 Element 树中的位置,因此可以用来:

  • 查找祖先提供的 InheritedWidget
  • 获取 ThemeMediaQueryNavigator 等上下文信息;
  • 访问 OverlayScaffold 等祖先结构;
  • 判断该位置是否仍然挂载。

BuildContext 不应被当作可长期保存的业务对象。尤其是异步操作完成后,原来的上下文可能已经不再挂载。


二、Widget、Element 和 State 的关系

可以用下面的关系表示:

flowchart TD
    W[Widget 不可变配置]
    E[Element 树节点]
    S[State 可变状态]
    R[RenderObject 渲染对象]
    C[BuildContext 接口]

    W -->|创建或更新| E
    E -->|StatefulElement 持有| S
    E -->|实现| C
    E -->|可选创建| R
    S -->|build 返回新的 Widget 子树| E

并不是每个 Widget 都直接对应一个 RenderObject:

  • Text 最终会产生渲染相关对象;
  • Builder 主要产生 Element,不直接负责绘制;
  • Column 会产生多子节点结构;
  • StatelessWidgetStatefulWidget 本身是构建逻辑,不是绘制对象。

因此,Widget 生命周期、Element 生命周期和 RenderObject 生命周期不能混为一谈。本文重点讨论 Widget/Element/State 层;布局和绘制属于后续渲染阶段。


三、一个 Element 如何被创建

StatefulWidget 为例:

class Greeting extends StatefulWidget {
  const Greeting({super.key, required this.name});

  final String name;

  @override
  State<Greeting> createState() => _GreetingState();
}

class _GreetingState extends State<Greeting> {
  @override
  Widget build(BuildContext context) {
    return Text(widget.name);
  }
}

当框架第一次遇到 Greeting 时,主要过程是:

  1. 创建 Greeting Widget;
  2. 调用 createElement(),创建 StatefulElement
  3. 调用 createState(),创建 _GreetingState
  4. 将 State 与 StatefulElement 关联;
  5. 标记 State 已挂载;
  6. 调用 initState()
  7. 调用 didChangeDependencies()
  8. 调用 build(),生成子树。

概念上的流程如下:

sequenceDiagram
    participant F as Flutter 框架
    participant W as Greeting Widget
    participant E as StatefulElement
    participant S as _GreetingState

    F->>W: createElement()
    W-->>E: 创建 StatefulElement
    F->>W: createState()
    W-->>S: 创建 State
    F->>E: 关联 State,mounted = true
    F->>S: initState()
    F->>S: didChangeDependencies()
    F->>S: build(context)
    S-->>F: 返回 Widget 子树

initState 的用途是初始化只与该 State 实例有关的资源,例如:

  • 创建控制器;
  • 注册一次性的监听器;
  • 启动需要与当前 State 绑定的初始化流程。

initState 中必须调用:

@override
void initState() {
  super.initState();
  // 初始化逻辑
}

此时 State 已经挂载,但依赖关系初始化流程仍由框架管理。需要读取会触发依赖注册的上下文信息时,应使用 didChangeDependenciesbuild,而不是把所有依赖初始化都塞进 initState


四、State 的完整生命周期

对于一个典型的 State,生命周期可以概括为:

创建 State
  ↓
initState
  ↓
didChangeDependencies
  ↓
build
  ↓
父 Widget 更新:didUpdateWidget → build
  ↓
依赖变化:didChangeDependencies → build
  ↓
暂时移出树:deactivate
  ↓
重新插回:activate → build(可能发生)
  或永久移除:dispose

下面分别说明每个阶段。

1. initState

initState 每个 State 实例只调用一次。

适合做:

@override
void initState() {
  super.initState();
  _controller = AnimationController(
    vsync: this,
    duration: const Duration(milliseconds: 300),
  );
}

不适合在这里假设父 Widget 的后续配置永远不变。如果初始化逻辑依赖 widget.someParameter,当父组件以后传入新的参数时,需要在 didUpdateWidget 中处理变化。

2. didChangeDependencies

didChangeDependencies 在初始化阶段会调用一次,之后当 State 依赖的 InheritedWidget 发生变化时可能再次调用。

例如:

@override
Widget build(BuildContext context) {
  final locale = Localizations.localeOf(context);
  return Text(locale.languageCode);
}

这里通过 Localizations.localeOf(context) 建立了对祖先依赖的访问。若上层 Locale 变化,框架会通知相关 Element,State 可能经历:

didChangeDependencies()
build()

Theme.of(context)MediaQuery.of(context)Localizations.of(...) 等 API 可能读取 InheritedWidget。现代 Flutter 中部分 API 使用更细粒度的依赖机制,但核心原则仍是:通过上下文读取的祖先依赖可能导致重新构建。

适合在 didChangeDependencies 中执行依赖变化时需要重新计算的工作,例如根据 Locale 重新加载资源。不要把它当作“每次 build 前必然执行”的回调;普通的 setState 只会触发 build,不会自动触发 didChangeDependencies

3. build

build 可以调用很多次。它不是初始化函数,也不能假设只运行一次。

@override
Widget build(BuildContext context) {
  return Column(
    children: [
      Text(widget.title),
      Text('count: $_count'),
    ],
  );
}

调用 build 的原因可能包括:

  • 当前 State 调用了 setState
  • 父 Widget 重新构建,并更新了当前 Widget;
  • 祖先 InheritedWidget 发生变化;
  • Element 被重新激活;
  • 热重载后框架重新构建;
  • 其他框架内部更新路径。

build 应主要完成“当前状态到 Widget 子树”的纯映射。它可以读取状态、创建 Widget,但不应在每次调用时重复注册监听器、启动请求或追加副作用。

4. didUpdateWidget

当父组件提供了一个新的、但仍然匹配当前 Element 的 StatefulWidget 时,框架会:

  1. 将 State 的 widget 引用更新为新 Widget;
  2. 调用 didUpdateWidget(oldWidget)
  3. 随后调用 build()

示例:

class SearchResults extends StatefulWidget {
  const SearchResults({
    super.key,
    required this.query,
  });

  final String query;

  @override
  State<SearchResults> createState() => _SearchResultsState();
}

class _SearchResultsState extends State<SearchResults> {
  @override
  void didUpdateWidget(covariant SearchResults oldWidget) {
    super.didUpdateWidget(oldWidget);

    if (oldWidget.query != widget.query) {
      _reload(widget.query);
    }
  }

  void _reload(String query) {
    // 根据新的 query 重新加载
  }

  @override
  Widget build(BuildContext context) {
    return Text(widget.query);
  }
}

这里不能只在 initState 中加载数据,因为 initState 不会在 query 变化时再次调用。

如果在 didUpdateWidget 中调用 setState,通常没有必要,因为回调返回后框架本来就会调用 build。如果回调中修改了状态,直接修改后让后续 build 使用即可;调用 setState 可能只是冗余,而不是必须。

5. deactivate

deactivate 表示 Element 暂时从树中移除,但它仍可能被重新插入。

常见场景是带有 GlobalKey 的子树从一个位置移动到另一个位置。此时可能经历:

deactivate()
activate()

而不是立即销毁 State。

因此,不应在 deactivate 中释放只能在 dispose 中释放的长期资源,除非你明确知道该资源只在当前树位置有效。

6. activate

如果被暂时移除的 Element 在框架允许的时间内重新插入,框架会调用 activate。重新激活后,相关依赖可能变化,通常还会重新构建。

若资源在 deactivate 中被暂停,需要在 activate 中恢复;但大多数普通 StatefulWidget 不需要重写这两个方法。

7. dispose

dispose 表示 State 永久不再使用。通常需要释放:

  • AnimationController
  • TextEditingController
  • ScrollController
  • StreamSubscription
  • Timer
  • 手动注册的监听器。
@override
void dispose() {
  _subscription.cancel();
  _controller.dispose();
  super.dispose();
}

dispose 中也必须调用 super.dispose()。调用后:

mounted == false

此时不能再调用 setState


五、Widget 如何匹配到旧 Element

生命周期的核心不是“每次构建都创建一切”,而是 Flutter 需要判断新 Widget 能否复用旧 Element。

对于普通 Widget,关键匹配条件可以概括为:

oldWidget.runtimeType == newWidget.runtimeType &&
oldWidget.key == newWidget.key

Flutter API 中对应的概念是 Widget.canUpdate。两个条件都满足时,旧 Element 通常可以继续使用;否则旧 Element 被移除,新 Widget 创建新的 Element。

情况一:类型和 key 都相同

const Text('A')

更新为:

const Text('B')

类型都是 Text,且 key 都为空,因此 Element 可以复用。文本内容会更新,但并不是因为旧 Widget 被修改,而是因为同一个 Element 接收了新的 Widget 配置。

对 StatefulWidget:

旧 Counter(key: K)
新 Counter(key: K)
        ↓
复用 StatefulElement
        ↓
复用 State
        ↓
didUpdateWidget
        ↓
build

情况二:类型不同

Text('A')

更新为:

Icon(Icons.star)

即使两者都没有 key,类型不同也不能直接把原来的 Element 当成同一种配置使用。旧子树会被替换为新子树。

情况三:key 不同

Counter(key: const ValueKey('first'))

更新为:

Counter(key: const ValueKey('second'))

类型虽然相同,但 key 不同,因此不能匹配为同一个位置上的同一身份。旧 State 不会被继续用于新 Widget。

列表中的位置匹配

没有 key 时,列表子项通常按位置匹配:

Column(
  children: [
    Counter(),
    Counter(),
  ],
)

如果在头部插入一个子项,原来的第一个 State 可能被复用于新的第一个位置,原来的第二个 State 可能被复用于新的第二个位置。这会导致“状态跟着位置走”,而不是跟着业务实体走。

需要稳定身份时使用 key:

Column(
  children: users.map((user) {
    return UserRow(
      key: ValueKey(user.id),
      user: user,
    );
  }).toList(),
)

此时 ValueKey(user.id) 让状态更可能跟随用户实体移动。

这里的“更可能”需要精确理解:在同一父级的匹配范围内,Flutter 会利用 key 找到对应的旧 Element;key 必须稳定、唯一,并且其相等性应符合业务身份定义。


六、一次父子更新的真实顺序

假设有如下组件:

class Parent extends StatefulWidget {
  const Parent({super.key});

  @override
  State<Parent> createState() => _ParentState();
}

class _ParentState extends State<Parent> {
  int value = 0;

  @override
  Widget build(BuildContext context) {
    return Child(value: value);
  }
}

class Child extends StatefulWidget {
  const Child({super.key, required this.value});

  final int value;

  @override
  State<Child> createState() => _ChildState();
}

class _ChildState extends State<Child> {
  @override
  void didUpdateWidget(covariant Child oldWidget) {
    super.didUpdateWidget(oldWidget);
    debugPrint('Child.didUpdateWidget: '
        '${oldWidget.value} -> ${widget.value}');
  }

  @override
  Widget build(BuildContext context) {
    debugPrint('Child.build: ${widget.value}');
    return Text('${widget.value}');
  }
}

Parent 调用:

setState(() {
  value++;
});

典型的因果链是:

setState 回调立即执行
  ↓
Parent Element 被标记为 dirty
  ↓
下一次构建阶段重新执行 Parent.build
  ↓
Parent 返回 Child(value: 新值)
  ↓
新旧 Child 类型和 key 匹配
  ↓
复用 Child 的 StatefulElement 和 State
  ↓
更新 State.widget
  ↓
Child.didUpdateWidget(oldWidget)
  ↓
Child.build()

重要区别是:

  • setState 的回调是同步执行的;
  • setState 不会直接同步调用当前 build
  • setState 的作用是告诉框架“该 State 产生了影响界面的变化,需要重新构建”;
  • 实际构建发生在框架的构建阶段,通常与下一帧调度相关。

父组件和子组件谁先 build

Flutter 的构建系统会按照 Element 深度处理 dirty Element,通常保证父级先于子级处理。这是必要的,因为父级的 build 可能决定:

  • 子组件是否存在;
  • 子组件的类型;
  • 子组件的 key;
  • 子组件的新配置。

但不应把所有回调之间的全局顺序当成业务 API 契约。精确顺序还会受到 dirty 集合、插入、移除、依赖通知和当前是否处于构建阶段的影响。可以依赖的核心规则是:

  1. 父级决定子级配置;
  2. 匹配成功时复用 Element/State;
  3. StatefulWidget 配置变化先进入 didUpdateWidget,随后构建;
  4. 被移除的子树最终会进入 dispose,除非在允许的移动路径中重新激活。

七、可运行示例:观察生命周期日志

下面是一个可以直接放入 Flutter 工程 lib/main.dart 的示例。它展示:

  • 首次挂载;
  • setState
  • 父 Widget 更新子 Widget;
  • key 改变导致 State 替换;
  • dispose
  • 异步回调完成后的 mounted 检查。
import 'dart:async';

import 'package:flutter/material.dart';

void main() {
  runApp(const LifecycleApp());
}

class LifecycleApp extends StatelessWidget {
  const LifecycleApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: LifecycleHome(),
    );
  }
}

class LifecycleHome extends StatefulWidget {
  const LifecycleHome({super.key});

  @override
  State<LifecycleHome> createState() => _LifecycleHomeState();
}

class _LifecycleHomeState extends State<LifecycleHome> {
  int parentValue = 0;
  String childKey = 'stable';

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Lifecycle')),
      body: Center(
        child: LifecycleChild(
          key: ValueKey(childKey),
          value: parentValue,
        ),
      ),
      floatingActionButton: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          FloatingActionButton(
            heroTag: 'update',
            onPressed: () {
              setState(() {
                parentValue++;
              });
            },
            child: const Icon(Icons.refresh),
          ),
          const SizedBox(height: 12),
          FloatingActionButton(
            heroTag: 'replace',
            onPressed: () {
              setState(() {
                childKey = childKey == 'stable' ? 'new' : 'stable';
              });
            },
            child: const Icon(Icons.swap_horiz),
          ),
        ],
      ),
    );
  }
}

class LifecycleChild extends StatefulWidget {
  const LifecycleChild({
    super.key,
    required this.value,
  });

  final int value;

  @override
  State<LifecycleChild> createState() => _LifecycleChildState();
}

class _LifecycleChildState extends State<LifecycleChild> {
  late final Timer timer;

  @override
  void initState() {
    super.initState();
    debugPrint('initState, key=${widget.key}');

    timer = Timer(const Duration(seconds: 2), () {
      if (!mounted) {
        debugPrint('timer finished, but State is unmounted');
        return;
      }

      debugPrint('timer finished, State is still mounted');
    });
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    debugPrint('didChangeDependencies');
  }

  @override
  void didUpdateWidget(covariant LifecycleChild oldWidget) {
    super.didUpdateWidget(oldWidget);
    debugPrint(
      'didUpdateWidget: ${oldWidget.value} -> ${widget.value}',
    );
  }

  @override
  Widget build(BuildContext context) {
    debugPrint('build: value=${widget.value}');
    return Text(
      'value = ${widget.value}',
      style: Theme.of(context).textTheme.headlineMedium,
    );
  }

  @override
  void deactivate() {
    debugPrint('deactivate');
    super.deactivate();
  }

  @override
  void activate() {
    super.activate();
    debugPrint('activate');
  }

  @override
  void dispose() {
    debugPrint('dispose');
    timer.cancel();
    super.dispose();
  }
}

第一次显示页面时

典型日志类似:

initState, key=[<'stable'>]
didChangeDependencies
build: value=0

这里 didChangeDependencies 出现在首次 build 前,是 State 生命周期初始化的一部分。

点击第一个按钮

父组件的 parentValue 改变,但 key 不变,子 Widget 类型也不变:

didUpdateWidget: 0 -> 1
build: value=1

不会再次出现 initState,说明 _LifecycleChildState 被复用了。

点击第二个按钮

key 从 stable 变为 new

dispose
initState, key=[<'new'>]
didChangeDependencies
build: value=1

旧 State 被销毁,新 State 被创建。实际日志中销毁和创建相关日志的相邻顺序可能受当前子树更新过程影响,但不会出现旧 State 继续接收新 Widget 配置的情况。

这个示例中的 Timer 为什么要检查 mounted

如果用户在两秒内离开页面,Timer 仍可能完成。Timer 回调访问 State、调用 setState 或使用上下文前,必须确认对象仍然挂载。否则可能出现:

setState() called after dispose()

State.mounted 适合在 State 方法中检查:

if (!mounted) return;

如果使用的是一个保存下来的 BuildContext,可以检查:

if (!context.mounted) return;

八、setState 的作用和边界

setState 接收一个同步回调:

setState(() {
  _count++;
});

它的逻辑可以抽象为:

执行回调,修改状态
  ↓
通知 StatefulElement 变脏
  ↓
调度或加入构建范围
  ↓
下一次 build 读取新状态

因此下面的代码不会让 buildsetState 调用点立即同步执行:

debugPrint('before');

setState(() {
  _count++;
  debugPrint('inside setState');
});

debugPrint('after');

先执行的顺序是:

before
inside setState
after

之后框架才会在构建阶段调用 build

不要把异步操作放进 setState 回调

错误示例:

setState(() async {
  _data = await loadData();
});

setState 要求回调同步完成。正确做法是先等待,再在同步回调中提交结果:

Future<void> load() async {
  final result = await loadData();

  if (!mounted) return;

  setState(() {
    _data = result;
  });
}

如果组件在 await 期间被移除,mounted 检查可以阻止向已销毁 State 提交结果。

setState 不是并发取消机制

即使检查了 mounted,也不代表旧请求被取消。它只表示结果返回时是否还允许更新当前 State。

如果请求本身支持取消,应在 dispose 中取消;如果不支持取消,则至少要处理:

  • 组件已经销毁;
  • 请求顺序反转;
  • 新请求结果覆盖旧请求结果。

例如连续搜索时,先发出的请求可能后返回:

请求 A:query = flutter
请求 B:query = flutter lifecycle
B 先返回
A 后返回

仅检查 mounted 不能解决结果乱序。还需要请求序号、取消令牌或响应版本校验。


九、BuildContext 的生命周期和异步安全

1. Context 不是永久有效句柄

下面的写法有风险:

late BuildContext savedContext;

void remember(BuildContext context) {
  savedContext = context;
}

Future<void> later() async {
  await Future<void>.delayed(const Duration(seconds: 1));

  Navigator.of(savedContext).pop();
}

等待期间,保存该 context 的 Widget 可能已经被移除。此时 savedContext 仍是一个 Dart 引用,但它不再代表一个有效的挂载位置。

更安全的形式是使用回调执行时仍在作用域中的 context,并在异步间隙后检查:

Future<void> openNext(BuildContext context) async {
  await Future<void>.delayed(const Duration(milliseconds: 300));

  if (!context.mounted) return;

  await Navigator.of(context).push(
    MaterialPageRoute<void>(
      builder: (_) => const NextPage(),
    ),
  );
}

context.mounted 表示这个 BuildContext 所代表的 Element 当前是否仍挂载。它不能保证:

  • 页面仍然是最顶层页面;
  • 业务请求仍然有效;
  • 用户仍然希望执行操作;
  • 其他依赖对象没有变化。

它只解决“这个上下文是否已经失效”这一层问题。

2. State 和 BuildContext 的 mounted 有什么区别

在 State 类中:

if (!mounted) return;

检查的是 State 是否仍与 Element 关联。

在只拿到 context 的函数中:

if (!context.mounted) return;

检查的是该上下文对应的 Element 是否仍在树中。

两者都不能在 dispose 后继续使用相关对象。尤其不要通过 contextdispose 中启动新的导航或异步工作。


十、依赖变化为什么不等同于父组件更新

didUpdateWidgetdidChangeDependencies 由不同原因触发。

父组件传入的新 Widget

父级 build 返回新 Widget
  ↓
新旧 Widget 类型和 key 匹配
  ↓
更新 State.widget
  ↓
didUpdateWidget
  ↓
build

祖先 InheritedWidget 变化

祖先依赖对象更新
  ↓
依赖它的 Element 被通知
  ↓
didChangeDependencies
  ↓
build

例如:

class LocaleLabel extends StatefulWidget {
  const LocaleLabel({super.key});

  @override
  State<LocaleLabel> createState() => _LocaleLabelState();
}

class _LocaleLabelState extends State<LocaleLabel> {
  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    debugPrint('locale or another dependency changed');
  }

  @override
  Widget build(BuildContext context) {
    final locale = Localizations.localeOf(context);
    return Text(locale.languageCode);
  }
}

如果 LocaleLabel 自身的构造参数不变,但上层 Locale 变化,didUpdateWidget 可能不会调用,而 didChangeDependencies 可能调用。

反过来,如果父组件只修改传入参数,而该 State 没有读取会变化的 InheritedWidget,则通常是 didUpdateWidgetbuild,不一定有 didChangeDependencies


十一、首次构建、重建、重挂载和销毁的区别

这些概念经常被混淆。

重建(rebuild)

重建只意味着重新执行某个 Element 的 build。State 可以保持不变:

同一个 State
  ↓
多次 build

父组件重建、调用 setState、依赖变化,都可能引起重建。

重挂载(re-mount)

如果旧 Element 不能匹配新 Widget,旧子树会被移除并创建新 Element:

旧 Element → deactivate → dispose
新 Widget → 新 Element → 新 State → initState

如果 key 允许子树移动,可能是:

deactivate → activate

而不是 dispose

热重载(hot reload)

热重载用于开发期更新 Dart 代码。框架通常保留现有 State,并通过 reassemble 等机制重新应用代码变化。它不是正常用户运行时生命周期,也不能用来推断生产环境中的初始化或销毁顺序。

不要把热重载造成的现象,例如某些字段没有重新初始化,误认为是应用首次启动行为。若需要测试真实生命周期,应停止并重新运行应用,或彻底重建相关路由和状态。


十二、常见错误及其失败原因

错误一:在 build 中启动请求

@override
Widget build(BuildContext context) {
  fetchUser();
  return const CircularProgressIndicator();
}

失败原因是 build 可能执行很多次,导致重复请求:

build 第一次 → 请求 1
build 第二次 → 请求 2
build 第三次 → 请求 3

如果请求只应在 State 创建时执行,可以放入 initState;如果请求依赖 Widget 参数变化,则放入 initStatedidUpdateWidget,并处理取消和乱序。

错误二:把 initState 当作参数变化回调

@override
void initState() {
  super.initState();
  load(widget.userId);
}

如果父级后来把 userId1 改为 2initState 不会重新执行。需要:

@override
void didUpdateWidget(covariant UserPanel oldWidget) {
  super.didUpdateWidget(oldWidget);

  if (oldWidget.userId != widget.userId) {
    load(widget.userId);
  }
}

错误三:列表没有 key,状态跟错数据

ListView(
  children: items.map((item) {
    return EditableRow(item: item);
  }).toList(),
)

items 插入、删除或排序时,State 可能按位置复用,编辑框内容、展开状态等就可能出现在错误的业务项上。

修正:

ListView(
  children: items.map((item) {
    return EditableRow(
      key: ValueKey(item.id),
      item: item,
    );
  }).toList(),
)

前提是 item.id 在该列表的身份范围内稳定且唯一。

错误四:异步回调中无条件调用 setState

Future<void> refresh() async {
  final data = await repository.load();
  setState(() {
    _data = data;
  });
}

如果 await 期间页面退出,就会在 State 销毁后调用 setState。至少应写成:

Future<void> refresh() async {
  final data = await repository.load();

  if (!mounted) return;

  setState(() {
    _data = data;
  });
}

生产代码还要决定请求异常如何展示、重复刷新是否取消、旧结果是否允许覆盖新结果。

错误五:错误理解 didUpdateWidget

下面的回调表示“同一个 State 收到了新的 Widget 配置”,不是“组件一定重新创建”:

@override
void didUpdateWidget(covariant MyWidget oldWidget) {
  super.didUpdateWidget(oldWidget);
}

如果 key 改变或类型改变,旧 State 可能直接销毁,新 State 走 initState,不会在旧 State 上调用 didUpdateWidget 接收新配置。


十三、如何诊断生命周期问题

1. 在关键回调记录身份

调试时不要只打印业务值,还应打印 State 身份和 key:

void logLifecycle(String name) {
  debugPrint(
    '$name '
    'state=${identityHashCode(this)} '
    'widget=${widget.runtimeType} '
    'key=${widget.key} '
    'mounted=$mounted',
  );
}

这样可以区分:

同一个 State 的多次 build

和:

旧 State dispose + 新 State initState

2. 检查匹配条件

遇到状态丢失时,按下面顺序检查:

  1. 新旧 Widget 的 runtimeType 是否相同;
  2. key 是否相同;
  3. key 是否稳定,而不是每次 build 都随机生成;
  4. 列表是否发生插入、删除或排序;
  5. 是否使用了 GlobalKey 引起子树移动;
  6. State 是否在某次条件渲染中被真正移除。

错误示例:

MyPanel(
  key: UniqueKey(),
)

每次构建都会创建不同的 key,导致 Flutter 无法复用原来的 Element 和 State。除非确实需要强制重置子树,否则不要这样写。

3. 使用 Flutter Inspector 查看树

Flutter Inspector 可以查看 Widget、Element 和 RenderObject 的关系。实际诊断时需要注意:

  • Widget Inspector 显示的 Widget 配置不等于 State;
  • 某个 Widget 每次重建都可能是新对象;
  • Element 是否复用,才是判断 State 是否保留的关键;
  • RenderObject 的布局问题不一定是 Widget 生命周期问题。

可以结合 debugPrint、断点和 DevTools 的 Widget Inspector 观察同一个 State 是否跨帧保留。


十四、不同平台的生命周期边界

Android、iOS、桌面和 Web 使用同一套 Flutter Widget/Element/State 生命周期模型:

  • initStatebuilddidUpdateWidgetdispose 的语义基本一致;
  • Widget 匹配规则不因平台而改变;
  • BuildContext 的挂载规则不因平台而改变。

平台差异主要出现在应用生命周期,而不是 Widget 生命周期。例如:

  • Android 可能收到后台、暂停、恢复相关状态;
  • iOS 有前后台切换和系统终止场景;
  • 桌面平台有窗口最小化、关闭和多窗口差异;
  • Web 有浏览器标签页隐藏、刷新和页面卸载。

应用生命周期通常通过 WidgetsBindingObserver 等机制观察,不能把“应用进入后台”直接等同于某个页面 State 的 dispose。应用进入后台时,页面可能仍然挂载;应用被系统终止时,也不能依赖所有 Dart 对象都能正常执行 dispose 来持久化数据。

因此:

Widget dispose

表示某个 Element 子树不再使用,而:

应用后台/前台/暂停

表示宿主平台对整个应用进程或窗口的状态变化。两者属于不同层次。


十五、把完整更新过程串起来

考虑如下变化:

父组件的状态改变

完整推导如下:

  1. 父组件调用 setState
  2. setState 同步执行修改状态的回调;
  3. 父级 StatefulElement 被标记为 dirty;
  4. Flutter 在构建阶段重新执行父组件的 build
  5. 父组件返回新的子 Widget;
  6. 框架使用类型和 key 匹配新旧子 Widget;
  7. 若匹配成功,复用旧 Element;
  8. 对 StatefulWidget,复用旧 State,并更新 State.widget
  9. 调用 didUpdateWidget
  10. 调用该 State 的 build
  11. 子树继续递归匹配;
  12. 构建结果交给后续布局、绘制和合成阶段。

若第 7 步匹配失败,则流程变为:

旧 Element 被移除
  ↓
deactivate
  ↓
可能 activate,或最终 dispose
  ↓
新 Widget 创建新 Element
  ↓
StatefulWidget 创建新 State
  ↓
initState
  ↓
didChangeDependencies
  ↓
build

这解释了几个表面上相似、实际不同的现象:

  • 界面变化但 State 没变:通常是复用 Element 后重新 build;
  • 参数变化但 initState 没执行:因为 State 被复用,走的是 didUpdateWidget
  • 状态突然重置:通常是类型、key、父级位置或条件渲染导致 Element 无法复用;
  • 异步回调崩溃:通常是等待期间 Element/State 已经 dispose;
  • 主题或 Locale 变化触发 build:通常是依赖的 InheritedWidget 变化。

最终可以用一句更精确的话概括 Flutter Widget 生命周期:

Widget 描述某一轮配置,Element 保留树中的身份,State 保存 StatefulWidget 的可变数据,BuildContext 暴露 Element 的位置;每次更新先匹配 Widget 与 Element,再决定复用、更新、移动或销毁,随后按生命周期回调顺序生成新的 Widget 子树。


系列导航与关联阅读

官方资料

本文依据 Flutter 与 Dart 官方文档重新梳理;正文与示例由 WR BLOG 编写。