每个示例都是以 main_0x.dart 格式命名的,想看哪个示例,把它重命名为 main.dart ,运行即可。
Flutter 3.3.6 环境所有示例测试通过。
所有示例代码地址 gitee.com/iam17/flutt…
key 是用来做什么的
key 是 Widget ,Element 和 SemanticsNode 的身份标识。
key 总的来说分两种,一种是 Locakey ,一种是 GlobalKey。区别就在于 Locakey 的作用范围在兄弟节点之间,GlobalKey 的作用范围是全局。Locakey 在兄弟节点中唯一,GlobalKey在整个 App 唯一。
Localkey 的作用
我没有用key,也没什么问题啊?一般情况下,不用 指定 Key参数是没有什么问题的。但是如果要交换两个 widget 的位置呢?或是有 widget 被删除呢?这个时候如果没有 key,就无法确定这个 widget 去更新哪个 element 了。在key 为 null的情况下,如果类型相同,会更新同一位置的 element。
Localkey 的作用是用来决定同一 parent 下是否可以重用 element 的。
举个例子,有两个 Box Widget,点击 floatingActionButton 后,文本会交换,但是颜色没有交换。
完整代码点这里 gitee.com/iam17/flutt…
为什么会有这样的效果呢?这得从头说起。
为了提高效率,flutter 有三棵树,分别由 Widget,Element,RenderObject 组成。
- Widget:Widget 相当于是配置文件,是不可变的。想改变,只能用一个新 widget 替换原来的 widget
- Element:Element 是实例化的 Widget 对象,通过 Widget 的 createElement() 方法,根据 Widget数据生成。如果 widget 有相同的 runtimeType 并且有两同的 key, Element 可以根据新的 widget 进行 update.
- RenderObject:用于应用界面的布局和绘制,保存了元素的大小,布局等信息.
两个关键逻辑
一、 判断 newWidget 是否可以更新持有 oldWidget 的 element。
static bool canUpdate(Widget oldWidget, Widget newWidget) {
return oldWidget.runtimeType == newWidget.runtimeType
&& oldWidget.key == newWidget.key;
}
在本例中,box1,box2 的 runtimeType 都是 Box,key 都为 null,所以 box1,box2是可以互相更新的。
二、 element 更新逻辑
element 的更新逻辑在 Element 类的 updateChild 方法中。更新逻辑如下
用 newWidget 更新 child。
- chid==null && newWidget!=null , 用 newWidget create new child。
- child.widget == newWidget,如果位置不同,更新child的位置。
- newWidget==null && child!=null ,删除 child
- newWidget!=null && child!=null,根据逻辑一中的判断逻辑判断 child是否可以更新,如果可以更新,用 newWidget 更新 child,否则 dispose 原 child,重建新的 child。
- child==null && newWidget==null 什么都不做
本例中,会命中第 4 条逻辑,更新文本,颜色不变,因为 element 的位置没变。
如果想让 element 更新位置,那就需要加上 LocalKey 了
在代码文件 main_01.dart 中找到这句
var children = [const Box(text: 'one'), const Box(text: 'two')];
加上 LocalKey
var children = [
const Box(text: 'one',key: ValueKey('one'),
),
const Box(text: 'two',key: ValueKey('two'),),
];
加上 key 之后,当 box widget 交换的时候,命中第二条逻辑,box widget 在 parent 中的位置发生了改变 ,更新 child,也就是 对应 element 的位置。
你可能会疑惑,为什么没有命中第四条?因为 box1,box2 的父级,也就是 column 在调用 updateChild(Element? child, Widget? newWidget, Object? newSlot)方法的时候,已经根据 widget 的 key 找到了对应的 child,调整了 child 的出场顺序,所以会命中第 2 条逻辑。
column 执行 updateChilren 方法调用 updateChild 准备 oldChild 的逻辑如下
//准备要更新的 child
Element? oldChild;
//用来更新 oldChild 的 widget
final Widget newWidget = newWidgets[newChildrenTop];
if (haveOldChildren) {
final Key? key = newWidget.key;
if (key != null) {
//通过key 找到 newWidget 对应的child
oldChild = oldKeyedChildren![key];
if (oldChild != null) {
if (Widget.canUpdate(oldChild.widget, newWidget)) {
// we found a match!
// remove it from oldKeyedChildren so we don't unsync it later
oldKeyedChildren.remove(key);
} else {
// Not a match, let's pretend we didn't see it for now.
oldChild = null;
}
}
}
}
final Element newChild =
updateChild(oldChild, newWidget, slotFor(newChildrenTop, previousChild))!;
通过源码可以知道 key 的重要性,如果没有 key 是无法根据 widget 找到对应的 child 的。
如果没有 key ,可能无法更新对应的 element。如果有了 key,比如本例中 column 就可以把 widget 和 child 配对,更新child的位置。
思考一下,如果交换 Box 的同时,同时修改 text,会走element 更新逻辑的第几条?
答案在本文最后。
Local key 是抽象类,无法直接用,可以用它的子类,ValueKey,ObjectKey,UniqueKey,PageStorageKey。
ValueKey
构造函数
const ValueKey(this.value);
final T value;
可以这样生成 key ValueKey<String>('1'),但一般我们都会省略范型,直接写 ValueKey('1'),dart 会自己推断出类型。
ValueKey(1),ValueKey('1'),是两个不同的 valueKey,因为它们的类型不同。
ValueKey<int>(1),ValueKey<double>(1),是两个不同的 valueKey,虽然它们的值相同,但类型不同。
因为有值相同但类型不同的这种情况存在,所以在判断valueKey是否相等的时候,同时判断 类型和值
bool operator ==(Object other) {
if (other.runtimeType != runtimeType) {
return false;
}
return other is ValueKey<T>
&& other.value == value;
}
ValueKey 是最常用的 key,一般来说对于一个列表,如果有 id,用每个条目 的 id 作为 key 最合适不过。不要用索引作为 key,因为如果有删除或修改索引会发生改变,不能作为标识条目的作用。
ObjectKey
构造函数
const ObjectKey(this.value);
final Object? value;
看起来像是 ValueKey的具像化。T 具体为 Object。其实不然,ObjectKey 判断相等 和 ValueKey 不同。ValueKey 的相等条件是 类型相同且值相同,ObjectKey的相等条件是 类型相同且引用相同。
判断引用举例:
var o = new Object();
ObjectKey(o),ObjectKey(o)相同,o 是同一个ObjectObjectKey(const Object()),ObjectKey(const Object())相同ObjectKey([1]),ObjectKey([1])不同。ObjectKey(const [1]),ObjectKey(const [1])相同。ObjectKey(const [1]),ObjectKey(const [2])不同。ObjectKey(2,1+1),ObjectKey(1),相同。
我们尽量加上 const 关键字,有相同的 key,有利于优化。
StatelessWidget 子类和 StatefulWidget 子类加上 const , ObjectKey 也不同。
class Test extends StatelessWidget {
const Test({super.key});
@override
Widget build(BuildContext context) {
return Container();
}
}
print(ObjectKey(const Test()) == ObjectKey(const Test())); //false
UniqueKey
class UniqueKey extends LocalKey {
UniqueKey();
@override
String toString() => '[#${shortHash(this)}]';
}
没错,整个 UniqueKey 的代码就这么多,再往上看 LocalKey,key也什么都没有。每个 UniqueKey 的实例对象都是不相等的,它的应用场景是什么呢?
场景一 必须用不同key的场景 AnimatedSwitcher 的 child 不同才能进行动画 ,所以用 UniqueKey 比较合适。
只有 Widget 的 canUpdate 判定为假的时候,才会执行 Switcher 动画,因为类型没有变,所以只能改变 key,这里用 UniqueKey 保证每个 key 都不同,所以每次都会执行动画。
Text(
'$count',
key: UniqueKey(),
style: const TextStyle(
fontSize: 100, fontWeight: FontWeight.bold, color: Colors.blue),
),
场景二 为了省事,不需要为 value 值而烦恼。先把key 生成好,每次都复用这个 key
class _MyWidgetState extends State<MyWidget> {
var key1 = UniqueKey();
var key2 = UniqueKey();
@override
Widget build(BuildContext context) {
return Row(children: const [Text('text1',key:key1),Text('text2',key:key2)],)
}
}
key生成好了后,在 state 没 dispose之前是不变的。 每次调用 build 方法都会复用 这个 key。
PageStorageKey
PageStorageKey 是 ValueKey的子类,它的用处是保持滚动位置。
有两个tab ,第一个 tabview 有 PageStorageKey,第二个tabview 没有 key。结果就是,第一个tab 保持了 滚动位置 ,第二个 tab 失去了滚动位置。
关键代码就是加 PageStorageKey 的这句
ListView.builder(
key:const PageStorageKey(1)
要想操持滚动位置 必须用 PageStorageKey ,用其它类型的 key 是不行的。
使用 GlobalKey
在整个 app 范围内 Globalkey 是唯一的。GlobalKey不应该在build方法中初始化,否则会每次build都重建 GlobalKey。为了不让GlobalKeyt每次都重新生成,可以让State对象拥有GlobalKey对象,然后在 State.initState 的方法中初始化 GlobalKey。
应用场景有两个
一、获取相关的 BuildContext 或 State
示例很简单,就是一个按钮,点击后换颜色,完整代码 gitee.com/iam17/flutt…
主要代码如下:
var key = GlobalKey<_BoxState>();
MaterialApp(
home: Scaffold(
body: Box(key: key),
floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
floatingActionButton: ElevatedButton(
onPressed: () {
RenderBox box = key.currentContext?.findRenderObject() as RenderBox;
//获取全局坐标
print(box.localToGlobal(Offset.zero));
//获取 Widget
var w = key.currentWidget!;
print(w.runtimeType);
//获取 context
var context = key.currentContext!;
print('${context.size} size');
//换颜色
key.currentState?.changeColor();
},
child: const Text('换颜色')),
))
通过 GlobalKey 找到 element 的逻辑是这样的。
在 Element mount 的时候先注册 key
void mount(Element? parent, Object? newSlot) {
...
final Key? key = widget.key;
if (key is GlobalKey) {
owner!._registerGlobalKey(key, this);
}
}
其实注册就是加到 owner的 Map里
final Map<GlobalKey, Element> _globalKeyRegistry = <GlobalKey, Element>{};
用的时候根据注册的 key 找到 element
Element? get _currentElement => WidgetsBinding.instance.buildOwner!._globalKeyRegistry[this];
找到了 element 就找到了 currentContext ,因为 contenxt 就是 element ,通过 element 还能找到 widget 和 state。
GlobalKey currentState 属性返回的是范型, GlobalKey 的范型声明就是给它准备的。
T? get currentState;
用完之后,在 Element 的 unmount 方法里会删除注册
void unmount() {
...
final Key? key = _widget?.key;
if (key is GlobalKey) {
owner!._unregisterGlobalKey(key, this);
}
...
}
有人不敢用 GlobalKey,理由是 GlobalKey 的成本很高。我们了解了 GlobalKey 找到 element 整个过程,并没有成本高的地方。所以在需要的地方,放心用!
二、在层级改变时保持状态
完整代码地址gitee.com/iam17/flutt…
在有globalkey 的情况下,Box 状态保持,点重新build按钮,Box的颜色不变。现在我们去掉 globalkey
Builder(builder: (BuildContext context) {
_count++;
if (_count % 2 == 0) {
return Box(
// 去掉 glokeyKey 看看效果
// key: _globalKey,
);
} else {
return Center(
child: Box(
// 去掉 glokeyKey 看看效果
//key: _globalKey,
),
);
}
})
去掉 globalkey 之后,我们发现,每次 build,颜色都会改变,说明状态丢失,丢失的原因是,每次 Box 都会重建。
所谓的状态保持其实就是 Box 对应的 element 用 update 代替了 create,所以 state 才能保持。
所以在同父级下更新 widget,可以用 localKey,不同父级下更新 widget,可以用 globalkey。
相比较而言,通过 GlobalKey 移动子树的成本会高一些,但是相比于重建子树,GlobalKey 的方案开销还是要小。
最后回答一下前面提到的问题:如果交换 Box 的同时,同时修改 text,会走element 更新逻辑的第几条?
答案是走 第 4 条逻辑。