在Flutter
中每个Widget
的构造方法都提供了一个可选参数Key
,这个Key
有什么用呢?
1、案例
现在看一个小小的Demo,这个Demo实现的功能是:每点击一次删除按钮,移除屏幕上的第一个Widget,功能非常简单,代码如下:
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(),
);
}
}
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return DeleteItem();
}
}
class DeleteItem extends StatefulWidget {
@override
_DeleteItemState createState() => _DeleteItemState();
}
class _DeleteItemState extends State<DeleteItem> {
List <Item> _itemList = [
Item('AAAAAA'),
Item('BBBBBB'),
Item('CCCCCC'),
Item('DDDDDD'),
];
void _deleteItem() {
if (_itemList.isNotEmpty)
setState(() {
_itemList.removeAt(0);
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Key Demo'),
),
body: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: _itemList,
),
),
floatingActionButton: FloatingActionButton(
onPressed: _deleteItem,
tooltip: 'deleteItem',
child: Icon(Icons.delete),
),
);
}
}
class Item extends StatefulWidget {
final String title;
Item(this.title);
@override
_ItemState createState() => _ItemState();
}
class _ItemState extends State<Item> {
Color _color = Color.fromRGBO(Random().nextInt(256), Random().nextInt(256), Random().nextInt(256), 1);
@override
Widget build(BuildContext context) {
return Container(
color: _color,
child: Center(
child: Text(widget.title,style: TextStyle(color: Colors.white,fontSize: 20,),),
),
);
}
}
但是结果却跟我们想要实现的功能不一样,如下图展示:
点击按钮确实按照A,B,C,D的顺序被删除了,但是颜色却是按着D,C,B,A的顺序被删了,为什么会出现这个奇怪的问题呢?
我们来尝试解决这个问题。
- 方案一
将Item
这个Widget
有StatefulWidget
改为StatelessWidget
:
class Item extends StatelessWidget {
final String title;
Item(this.title);
final Color _color = Color.fromRGBO(Random().nextInt(256), Random().nextInt(256), Random().nextInt(256), 1);
@override
Widget build(BuildContext context) {
return Container(
color: _color,
child: Center(
child: Text(title,style: TextStyle(color: Colors.white,fontSize: 20,),),
),
);
}
}
这次重现象上看是正常了,满足了需求:
- 方案二
Item
这个Widget
还是StatefulWidget
,之前_color
是保存在State
里面,现在修改为放在Widget
中:
class Item extends StatefulWidget {
final String title;
Item(this.title);
final Color _color = Color.fromRGBO(
Random().nextInt(256), Random().nextInt(256), Random().nextInt(256), 1);
@override
_ItemState createState() => _ItemState();
}
class _ItemState extends State<Item> {
@override
Widget build(BuildContext context) {
return Container(
color: widget._color,
child: Center(
child: Text(
widget.title,
style: TextStyle(
color: Colors.white,
fontSize: 20,
),
),
),
);
}
}
看一下结果:
也是满足需求的。
来分析一下原因。
首先要明确一点,Widget在重新build的时候,是增量更新的,而不是全部更新,那怎么实现增量更新的呢。Widget树生成的时候,Element树也同步生成,Widget会判断是否要更新Element的widget:
/// Whether the `newWidget` can be used to update an [Element] that currently
/// has the `oldWidget` as its configuration.
///
/// An element that uses a given widget as its configuration can be updated to
/// use another widget as its configuration if, and only if, the two widgets
/// have [runtimeType] and [key] properties that are [operator==].
///
/// If the widgets have no key (their key is null), then they are considered a
/// match if they have the same type, even if their children are completely
/// different.
static bool canUpdate(Widget oldWidget, Widget newWidget) {
return oldWidget.runtimeType == newWidget.runtimeType
&& oldWidget.key == newWidget.key;
}
这个方法的意义在于,Element会判断新旧Widget的runtimeType和key是否同时相等,如果相等就说明新旧的配置相同,如果相同就可以更新使用另一个小部件。
-
先看看为什么最开始的代码不可以满足需求。
第一次构建完页面,Widget树和Element树的关系如下:
Element
树中的element
与Widget
树中的widget
是一一对应的,element
对象持有widget
和state
两个对象,Item
的title
保存在widget
中,而_color
保存state
中,如上图展示。当我们删除了第一个Item-AAAAAA
之后,重新build
时候这时候由于AAAAAA
被删除了,BBBBBB
的Index
就变成了0
,Element
树就会拿Index
为0
的widget
与Widget
树的index
为0
的widget
比较,看看是否更新widget
,比较的内容就是runtimeType
和key
,由于AAAAAA
和BBBBBB
的类型都是Item
,并且key
为null
,所以Element
就认为widget
没有发生变化,如实就更新了widget
,但是state
没有发生变化:
同理接着 build
CCCCCC
的时候Element
会拿Index
为1的element
里面的widget
与Widget
树里Index
为1
的Element
作比较,同样是可以更新的,如是就更新了widget
但是state
仍然没变:
同理 build
DDDDDD
:
build
完DDDDDD
之后,Element
发现有多的element
没有widget
与之对应,就把多的element
对象干掉了:
上面就解释了为什么我们看到的是Title被正常删除了,但是颜色却是从后往前删的原因了。
那为什么方案一
和方案二
都可以解决这个问题呢,不管是方案一
还是方案二
都是将颜色也放到了element
的widget
中了,这样在更新widget
的时候,title
和color
就一起更新了。
产生这个问题的根本原因是在更新的时候新旧widget的runtimeType和key都是一样的,runtimeType都是Item类型这个不会改变,那我们可以在创建每个Item时给它一个key,这样在比较widget的时候由于key不一样就不会更新而是重新生成新的element就可以解决问题了:
List<Item> _itemList = [
Item('AAAAAA',key: ValueKey(111111),),
Item('BBBBBB',key: ValueKey(222222),),
Item('CCCCCC',key: ValueKey(333333),),
Item('DDDDDD',key: ValueKey(444444),),
];
class Item extends StatefulWidget {
final String title;
Item(this.title,{Key key}):super(key:key);
@override
_ItemState createState() => _ItemState();
}
class _ItemState extends State<Item> {
final Color _color = Color.fromRGBO(
Random().nextInt(256), Random().nextInt(256), Random().nextInt(256), 1);
@override
Widget build(BuildContext context) {
return Container(
color: _color,
child: Center(
child: Text(
widget.title,
style: TextStyle(
color: Colors.white,
fontSize: 20,
),
),
),
);
}
}
添加一个key之后,widget里面就多了一个变量key,生成之后Widget树和Element树为:
当删除AAAAAA
之后,重新构建BBBBBB
的时候:
widget
还是会取element
的树index
为0
的widget
来和自己比较看是否更新,这时key
是不一致的,所以element
就会被干掉重新生成了:
生成
CCCCCC
、DDDDDD
是同样的道理,这里就不展示了。这样就可以实现需求了。
2、Key的类型及作用
Key
本身是一个虚类定义如下:
@immutable
abstract class Key {
/// Construct a [ValueKey<String>] with the given [String].
///
/// This is the simplest way to create keys.
const factory Key(String value) = ValueKey<String>;
/// Default constructor, used by subclasses.
///
/// Useful so that subclasses can call us, because the [new Key] factory
/// constructor shadows the implicit constructor.
@protected
const Key.empty();
}
它的直接子类型有两个LocalKey
和GlobalKey
两种。
2.1 LocalKey
LocalKey
是diff
算法的核心所在,用做Element
和Widget
的比较。常用子类有以下几个:
-
ValueKey
:以一个数据为作为key
,比如数字、字符等。 -
ObjectKey
:以Object
对象作为Key
。 -
UniqueKey
:可以保证key
的唯一性,如果使用这个类型的key
,那么Element
对象将不会被复用。 -
PageStorageKey
:用于存储页面滚动位置的key
。
2.2 GlobalKey
每个globalkey
都是一个在整个应用内唯一的key
。globalkey
相对而言是比较昂贵的,如果你并不需要globalkey
的某些特性,那么可以考虑使用Key
、ValueKey
、ObjectKey
或UniqueKey
。
他有两个用途:
- 用途1:允许widget在应用程序中的任何位置更改其parent而不丢失其状态。应用场景:在两个不同的屏幕上显示相同的widget,并保持状态相同。
- 用途2:可以获取对应Widget的state对象:
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(),
);
}
}
class MyHomePage extends StatelessWidget {
final GlobalKey<_CounterState> _globalKey = GlobalKey();
void _addCounter() {
_globalKey.currentState.description = '旧值:'+ _globalKey.currentState.count.toString();
_globalKey.currentState.count ++;
_globalKey.currentState.setState(() {});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Key Demo'),
),
body: Counter(key: _globalKey,),
floatingActionButton: FloatingActionButton(
onPressed: _addCounter,
tooltip: 'deleteItem',
child: Icon(Icons.add),
),
);
}
}
class Counter extends StatefulWidget {
Counter({Key key}) : super(key: key);
@override
_CounterState createState() => _CounterState();
}
class _CounterState extends State<Counter> {
String description = '旧值';
int count = 0;
@override
Widget build(BuildContext context) {
return Center(
child: Column(children: [
Text(
'$count',
style: TextStyle(
color: Colors.black,
fontSize: 20,
),
),
Text(
description,
style: TextStyle(
color: Colors.black,
fontSize: 20,
),
),
]),
);
}
}
这个例子比较简单,展示通过GlobalKey
获取子Widget
的State
并更新。