Flutter的所有Widget的构造函数都有一个optional的Key参数,你可以指定,也可以不指定。
在用Flutter进行开发时,大多数情况下,我们并不需要为widget指定key。但今天我却碰到了一个由key引发的问题。我有一个ListView,我要让用户可以删掉其中的任何item。当我将某个数据从数组中删掉,然后setState时,发现列表没有变化。因为之前有写过一点React,知道可能是Widget的key导致的,于是给列表里的每个item加了key,问题立马就解决了。问题解决并不是终点,我们需要搞清楚Flutter的key到底是个什么东西。
Flutter的UI有2棵树,一棵Widget树,一颗Element树。Flutter实际上是用Element树以及它对应的State来渲染UI的。
Element类有对应Widget的引用:
Element类也有对应Widget State的引用:
让我们用一个简单的demo来演示没有key导致的问题。我有2个色块,我点击一个按钮去交换这两个按钮的位置。
void main() => runApp(new MaterialApp(home: PositionedTiles()));
class PositionedTiles extends StatefulWidget {
@override
State<StatefulWidget> createState() => PositionedTilesState();
}
class PositionedTilesState extends State<PositionedTiles> {
List<Widget> tiles = [
StatefulColorfulTile(),
StatefulColorfulTile(),
];
@override
Widget build(BuildContext context) {
return Scaffold(
body: Row(children: tiles),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.sentiment_very_satisfied), onPressed: swapTiles),
);
}
swapTiles() {
setState(() {
tiles.insert(1, tiles.removeAt(0));
});
}
}
class StatefulColorfulTile extends StatefulWidget {
@override
ColorfulTileState createState() => ColorfulTileState();
}
class ColorfulTileState extends State<ColorfulTile> {
Color myColor;
@override
void initState() {
super.initState();
myColor = UniqueColorGenerator.getColor();
}
@override
Widget build(BuildContext context) {
return Container(
color: myColor,
child: Padding(
padding: EdgeInsets.all(70.0),
));
}
}
实际结果是,点击后,这2个色块并不会交换位置。
当色块用StatelessWidget时,是没有问题的,色块可以正常交换位置。原因是,当Widget tree变化之后,Flutter框架开始遍历Element tree,对比它跟Widget tree的差异,并做更新。对比差异时,框架会对比Widget的类型和key,但因为没有指定key,所以只对比类型。框架发现Element tree里第一个Tile的类型跟Widget tree里的一样,于是就更新它的Widget引用,这时就引用到了交换后的色块了。第二个Element元素也是一样的过程,会更新引用,引用交换后的色块。这就是为什么用StatelessWidget没有问题的原因。
但StatefulWidget的情况不一样。StatefulWidget的State并不是由Widget引用的,而是由Element引用的,如下图所示:
这时虽然Element指向了交换后Widget,但引用的State类并没有变,所以框架在渲染时并没有变化。
当我们给每个Widget加了key之后,情况就不一样了。框架在对比Element和Widget时,发现key不一样,于是将这个Element从Element tree里移除。这样的话,2个色块的Element都会被移除。之后框架得重建这些Element。为了提高框架的效率,必须有Element的复用机制,所以框架会在同一个父Element之下被移除的子元素中找key一样的,找到后Element tree再引用它们。这样的话Element tree里的2个Element就交换了位置,渲染到屏幕上时也就对了。
所以请记住:
当你需要插入、删除、重排序一个类型相同,且是Stateful的widget列表时,你需要给每个widget指定key。
References:
https://medium.com/flutter/keys-what-are-they-good-for-13cb51742e7d