概述
在Widget的构造方法中,有Key这么一个可选参数,Key是一个抽象类,有LocalKey和GlobalKey两种,本文将对这两种key的作用进行探究。
LocalKey
在探究LocalKey的作用之前,先通过一段代码来看看一个场景
class LocalKeyDemo extends StatefulWidget {
@override
_LocalKeyDemoState createState() => _LocalKeyDemoState();
}
class _LocalKeyDemoState extends State<LocalKeyDemo> {
List<TitleItem> _items = [
TitleItem(title: "aaaaa"),
TitleItem(title: "bbbbb"),
TitleItem(title: "ccccc"),
];
@override
Widget build(BuildContext context) {
return Container(
child: Scaffold(
appBar: AppBar(
title: Text("Key"),
),
body: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: _items,
),
floatingActionButton: FloatingActionButton(
onPressed: (){
setState(() {
_items.removeAt(0);
});
},
child: Icon(Icons.delete),
),
),
);
}
}
class TitleItem extends StatefulWidget {
final String title;
TitleItem({this.title});
@override
_TitleItemState createState() => _TitleItemState();
}
class _TitleItemState extends State<TitleItem> {
final Color _randomColor = Color.fromRGBO(Random().nextInt(256), Random().nextInt(256), Random().nextInt(256), 1);
@override
Widget build(BuildContext context) {
return Container(
color: _randomColor,
width: 100,
height: 100,
child: Center(
child: Text(widget.title),
),
);
}
}
这段代码中创建了3个颜色不同,文本不同的子widget,按照从左往右排列,要注意的是,颜色对象保存在Sate中,而文本对象保存在Widget中,然后点击按钮,每次删除最左边的widget。
执行结果出乎意料:
虽然每次都能删除文本正确的widget,但是颜色值确实下一个widget的颜色,这种奇怪现象的原因其实和Flutter的增量渲染机制有关,我们知道widget树是对应着element树的,而widget树是不稳定的,widget在每次的刷新中都有可能在创建或者销毁,而element不会,他会去对比树中新的widget和旧widget是否一致,是否可以更新widget,如果可以更新,那么element将会指向新的widget。如下图:
一开始,widget树的节点与element树的节点是一一对应的,element的_widget属性指向了对应的widget,当widget树发生改变
element树并不会全部重新创建,而是与从左到右比较,看是否新的节点的widget与原先的节点的widget是否一致,而判断的依据我们可以在源码中看到
static bool canUpdate(Widget oldWidget, Widget newWidget) {
return oldWidget.runtimeType == newWidget.runtimeType
&& oldWidget.key == newWidget.key;
}
他比较的新旧widget的runtimeType和key值,明显widgetA和widgetB的runtimeType和key均一致,所以element会变成这样
element会复用,而widget则换成新的widget,当state并没有改变,而在案例中颜色是存放在sate中的,所以,首个element的文本变成了widgetB的文本,但是颜色还是sateA的的颜色,而widgetC的颜色变成了stateB的颜色,而由于原先3个节点变成两个,所以最后一个element被移除,最终变成我们看到的结果。
要解决这个问题,我们只需要在Flutter判断是否更新widget的时候给一个false的结果,就能解决这个问题,两个对比条件中,runtimeType肯定是一致的,所以,可以给widget添加一个唯一标示key
我们只需要对代码做下面的修改
...
class _LocalKeyDemoState extends State<LocalKeyDemo> {
List<TitleItem> _items = [
TitleItem(title: "aaaaa", key: ValueKey(1),),
TitleItem(title: "bbbbb", key: ValueKey(2),),
TitleItem(title: "ccccc", key: ValueKey(3),),
];
...
}
...
class TitleItem extends StatefulWidget {
final String title;
TitleItem({this.title, Key key}) : super(key: key);
@override
_TitleItemState createState() => _TitleItemState();
}
...
这样,结果就是正常的了。
从上面的案例可以看出,LocalKey可以给Widget作为唯一表示,在element树更新能准确的更新对应正确的widget。
LocalKey除了ValueKey,还有ObjectKey和UniqueKey两种类型,其实效果都一样,只是有一些差别
- ValueKey: 以一个数据作为Key,如:数字、字符
- ObjectKey: 以Object对象作为Key
- UniqueKey: 可以保证Key的唯一性,一旦使用UniqueKey那么就不存在Element复用了
GlobalKey
GlobalKey可以获取到对应的Sate,Element以及Widget,
BuildContext get currentContext => _currentElement;
Widget get currentWidget => _currentElement?.widget;
T get currentState {
final Element element = _currentElement;
if (element is StatefulElement) {
final StatefulElement statefulElement = element;
final State state = statefulElement.state;
if (state is T)
return state;
}
return null;
}
}
而利用这个特性,我们可以实现局部刷新从而进行优化,比如如果只是根widget的按钮被点击,而需要改变的仅仅是子widget,我们并不需要刷新整个widget树,可以通过GlobalKey拿到对应的sate,仅仅刷新子widget的状态,从而优化性能。
class GlobalKeyDemo extends StatelessWidget {
final GlobalKey<_ChildPageState> _globalKey = GlobalKey();
@override
Widget build(BuildContext context) {
return Container(
child: Scaffold(
appBar: AppBar(
title: Text("GlobalKey"),
),
body: ChildPage(key: _globalKey),
floatingActionButton: FloatingActionButton(
onPressed: () {
_globalKey.currentState.setState(() {
_globalKey.currentState.count ++;
});
},
child: Icon(Icons.add),
),
),
);
}
}
class ChildPage extends StatefulWidget {
ChildPage({Key key}):super(key: key);
@override
_ChildPageState createState() => _ChildPageState();
}
class _ChildPageState extends State<ChildPage> {
int count = 0;
@override
Widget build(BuildContext context) {
return Container(
child: Center(
child: Text(count.toString(), style: TextStyle(fontSize: 30, fontWeight: FontWeight.w800, color: Colors.blue),),
),
);
}
}
总结
以上,便是我对key的探究,Key是一个抽象类,有LocalKey和GlobalKey两个子类,LocalKey可以作为Widget的唯一标示,避免Element的重用,而GlobalKey可以拿到指定的Widget、Element、State。