"位置变动"有两种完全不同的情况:
- 在同一父节点下,兄弟节点之间的位置变动(重排序)。
- 跨越不同父节点的位置变动(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 的。