"位置变动"有两种完全不同的情况:
- 在同一父节点下,兄弟节点之间的位置变动(重排序)。
- 跨越不同父节点的位置变动(Reparenting / 移植)。
1. 兄弟节点之间的位置变动 (LocalKey 的主场)
这是 LocalKey 设计的核心应用场景。比如在一个 Column、Row 或者 ListView 中。
例子:交换两个带 LocalKey 的 Widget
dart
// 初始状态
Column(
children: [
StatefulWidgetA(key: ValueKey('A')),
StatefulWidgetB(key: ValueKey('B')),
],
)
当 setState 之后,我们交换它们的位置:
dart
// 更新后的状态
Column(
children: [
StatefulWidgetB(key: ValueKey('B')),
StatefulWidgetA(key: ValueKey('A')),
],
)
Flutter 的处理流程 (使用 LocalKey):
- Flutter 看到新的子 Widget 列表是
[WidgetB, WidgetA]。 - 它看到旧的子 Element 列表是
[ElementA, ElementB]。 - 对于新的第一个 Widget
WidgetB(key: ValueKey('B')),它在旧的兄弟节点 中查找ValueKey('B')。 - 找到了!是旧的
ElementB。Flutter 会复用ElementB及其 State,并把它更新到新的位置(索引 0)。 - 同理,
WidgetA也会找到并复用ElementA。
结论: 在这种情况下,LocalKey 完美地保留了 State 。这是它的核心功能。如果你发现没有保留,通常是因为 Key 设置错误(比如用了 UniqueKey(),它每次 build 都会生成新的 Key),或者 Widget 类型发生了变化。
2. 跨父节点的位置变动 (GlobalKey 的主场)
这是你问题中描述的"位置变动"最可能指向的场景,也是 LocalKey 无能为力的场景。
例子:将 Widget 从一个 Container 移动到另一个 Container
dart
// 假设 isFirstParent 为 true
Column(
children: [
Container( // 父节点1
color: Colors.blue,
child: isFirstParent ? StatefulWidgetA(key: ValueKey('A')) : SizedBox(),
),
Container( // 父节点2
color: Colors.red,
child: !isFirstParent ? StatefulWidgetA(key: ValueKey('A')) : SizedBox(),
),
],
)
当 setState 之后,我们把 isFirstParent 设为 false。
Flutter 的处理流程 (使用 LocalKey):
-
处理父节点1 (
Container blue):- 旧的子 Widget 是
StatefulWidgetA(key: ValueKey('A'))。 - 新的子 Widget 是
SizedBox()。 - 由于类型和 Key 都不匹配,Flutter 会销毁
StatefulWidgetA对应的Element和State。它不会去别的地方寻找这个 Key。
- 旧的子 Widget 是
-
处理父节点2 (
Container red):- 旧的子 Widget 是
SizedBox()。 - 新的子 Widget 是
StatefulWidgetA(key: ValueKey('A'))。 - 这是一个全新的创建过程。Flutter 会为
StatefulWidgetA创建一个新的 Element 和一个新的 State。
- 旧的子 Widget 是
结论: 在这种情况下,StatefulWidgetA 的状态丢失了 。因为 LocalKey 的查找范围仅限于其原来的兄弟节点 。当 StatefulWidgetA 从 Container blue 中被移除时,它的 Element 就被释放了。Container red 在构建自己的子节点时,无法"看到"或"访问"那个刚刚被销毁的 Element。
为什么 GlobalKey 可以做到?
现在我们把 ValueKey('A') 换成 GlobalKey()。
Flutter 的处理流程 (使用 GlobalKey):
-
当
setState触发重建时,Flutter 有一个全局的 Map 记录着所有激活的GlobalKey和它们对应的Element。Map<GlobalKey, Element>。 -
处理父节点1 (
Container blue):- Flutter 看到
StatefulWidgetA将要被移除。但它注意到这个 Widget 有一个GlobalKey。 - 它不会立即销毁
ElementA。而是暂时将它从树中分离出来,但保留在内存中,并标记为"待认领"。
- Flutter 看到
-
处理父节点2 (
Container red):- Flutter 看到这里需要构建一个新的
StatefulWidgetA,并且它带有一个GlobalKey。 - 它会去全局的
Map中检查:"嘿,这个GlobalKey我是不是在哪里见过?" - 它发现这个
GlobalKey正指向那个刚刚从Container blue中分离出来的、"待认领"的ElementA。 - Bingo! Flutter 不会创建新的
Element,而是直接将这个旧的ElementA(连同它宝贵的State)"移植" 过来,挂载到Container red下面。
- Flutter 看到这里需要构建一个新的
结论: GlobalKey 通过一个全局注册表 ,实现了 Element 和 State 的跨父级迁移,从而保留了状态。
总结与类比
-
LocalKey就像一个班级里的学生学号。- 在班级内部(兄弟节点),老师可以通过学号准确找到张三李四,即使他们换了座位(重排序),也能认出他们。
- 但如果张三转学到了另一个班级(跨父节点),新班级的老师是不知道他原来的学号的,会给他一个新的学号(创建新 State)。
-
GlobalKey就像一个全国唯一的身份证号。- 无论张三转到哪个学校、哪个班级(在 Widget 树中如何移动),只要出示身份证,系统就能识别出"哦,还是原来那个张三",他所有的个人档案(State)都会被完整地迁移过来。
LocalKey 的 Widget 在"跨父级"位置变动后,State 没有保留,是因为 LocalKey 的作用域是局部的,它无法实现 Element 的"跨级传送"。 而在"同级"位置变动(重排序)时,LocalKey 是会保留 State 的。