LocalKey
也会复用 Element
和 State
。实际上,这正是 LocalKey
最主要、最核心的作用。
GlobalKey
和 LocalKey
的区别不在于是否 复用 Element
/State
,而在于在多大的范围内寻找 那个需要被复用的 Element
。
LocalKey 是如何处理 Element 和 State 的?
为了理解这一点,我们需要简单了解一下 Flutter 的更新机制。
-
Widget 树 vs. Element 树:
- 你写的代码是 Widget 树,它是一个"配置"或"蓝图",描述了 UI 应该长什么样。Widget 是不可变的(immutable)。
- Flutter 内部维护一个 Element 树 。Element 是 Widget 的一个实例化对象,它持有对 Widget 和
State
(如果是StatefulWidget
)的引用,并且是连接到最终渲染对象的桥梁。Element 是可变的,并且生命周期更长。
-
更新过程(Reconciliation / Diffing):
- 当你调用
setState()
时,Flutter 会标记对应的 Element 为 "dirty" (需要更新)。 - 在下一帧,Flutter 会重新执行
build
方法,生成一个新的 Widget 树。 - 然后,Flutter 会遍历新旧 Widget 树(实际上是遍历新的 Widget 树,并与旧的 Element 树进行比较),来决定如何最高效地更新 Element 树。
- 当你调用
现在,LocalKey
在这个比较过程中扮演了至关重要的角色。
场景一:没有 Key 的情况
当 Flutter 比较一个父 Widget 的新旧子节点列表时,如果没有 Key,它会按顺序和类型进行匹配。
例子:一个可删除的列表项
假设我们有一个列表,包含三个带状态的 StatefulTile
(比如每个都有自己随机生成的颜色)。
初始状态:
- Widget 树 :
[StatefulTile(A), StatefulTile(B), StatefulTile(C)]
- Element 树 :
[Element(A), Element(B), Element(C)]
(每个 Element 都持有自己的 State,比如颜色)
操作:删除第一个元素 A
。
setState
后,新的 build
方法返回:
- 新 Widget 树 :
[StatefulTile(B), StatefulTile(C)]
Flutter 的比对过程 (无 Key):
-
比较位置 0:
- 新 Widget 是
StatefulTile(B)
。 - 旧 Element 是
Element(A)
。 - 类型匹配吗? 是的,都是
StatefulTile
。 - 结果 :Flutter 认为你只是想更新 这个位置的 Widget。于是,它用
StatefulTile(B)
的配置去更新Element(A)
。Element(A)
和它内部的 State (A的颜色) 被保留了下来!
- 新 Widget 是
-
比较位置 1:
- 新 Widget 是
StatefulTile(C)
。 - 旧 Element 是
Element(B)
。 - 类型匹配吗? 是的。
- 结果 :用
StatefulTile(C)
的配置去更新Element(B)
。Element(B)
和它的 State (B的颜色) 被保留。
- 新 Widget 是
-
比较结束:
- 旧的
Element(C)
没有被匹配到,于是它被销毁。
- 旧的
最终效果: 屏幕上显示了两个 Tile。第一个 Tile 的内容是 B,但颜色是 A 的颜色。第二个 Tile 的内容是 C,但颜色是 B 的颜色。状态错乱了!
场景二:使用 LocalKey 的情况
现在,我们给每个 StatefulTile
一个基于其内容的 LocalKey
(比如 ValueKey
)。
初始状态:
- Widget 树 :
[StatefulTile(key: ValueKey('A')), StatefulTile(key: ValueKey('B')), ...]
- Element 树 :
[Element(key: ValueKey('A')), Element(key: ValueKey('B')), ...]
操作:删除第一个元素 A
。
setState
后,新的 build
方法返回:
- 新 Widget 树 :
[StatefulTile(key: ValueKey('B')), StatefulTile(key: ValueKey('C'))]
Flutter 的比对过程 (有 Key):
- Flutter 拿到新的 Widget 列表
[Widget(B), Widget(C)]
和旧的 Element 列表[Element(A), Element(B), Element(C)]
。 - 它不再仅仅按顺序比较,而是优先使用 Key 来查找匹配项。
- 对于新的第一个 Widget
StatefulTile(key: ValueKey('B'))
,Flutter 会在旧的兄弟节点中 寻找一个 Key 也是ValueKey('B')
的 Element。 - 它找到了!是旧列表中的第二个 Element,
Element(B)
。 - 结果 :Flutter 复用
Element(B)
(连同它正确的 State),并把它移动到新的位置(位置 0)。 - 同理,对于新的第二个 Widget
StatefulTile(key: ValueKey('C'))
,Flutter 找到了旧的Element(C)
并复用它。 - 旧的
Element(A)
在新 Widget 列表中没有找到匹配的 Key,于是它被正确地销毁。
最终效果: 屏幕上正确地显示了 Tile B 和 Tile C,并且它们都保留了自己原有的、正确的状态(颜色)。
LocalKey vs GlobalKey 的复用机制对比
特性 | LocalKey (如 ValueKey, ObjectKey) | GlobalKey |
---|---|---|
查找范围 | 仅在兄弟节点之间 (Siblings)。Flutter 只会在同一个父 Widget 的子节点中查找匹配的 Key。 | 全局 (Entire App)。Flutter 会在整个应用范围内查找匹配的 Key。 |
核心目的 | 在一个集合(如 ListView , Column )内高效地 识别、重排、添加、删除 元素,并正确复用其 State 。 |
1. 跨越 Widget 树的层级,从外部访问一个 Widget 的 State 。 2. 将一个 Widget 连同其 State 在不同的父节点之间移动。 |
性能 | 高。查找范围小,非常高效。 | 较低。需要维护一个全局的 Map,查找开销更大。 |
总结
LocalKey
和GlobalKey
都会 触发Element
和State
的复用。LocalKey
的作用域是局部的(兄弟节点之间) 。它是 Flutter 在处理列表、集合等动态 UI 时,保证状态正确性和渲染效率的基石。当你遇到列表项重排、删除后状态不正确的问题时,第一个就应该想到使用LocalKey
。GlobalKey
的作用域是全局的。它处理的是更特殊的情况,比如跨父级移动 Widget 或从任意位置访问特定 Widget 的状态,这是一种"降维打击",但开销也更大,需要谨慎使用。