在Widget的构造方法中,有Key这么一个可选参数,用作Element和Widget进行比较
Key是一个抽象类,有LocalKey和GlobalKey两种

LocalKey

ValueKey,ObjectKey和UniqueKey三种类型:

  • ValueKey: 以一个数据作为Key,如:数字、字符
  • ObjectKey: 以Object对象作为Key
  • UniqueKey: 可以保证Key的唯一性,一旦使用UniqueKey那么就不存在Element复用了,只要刷新Widget就好重建Element

当不传入Key时,按照Element的增量渲染,可能会存在问题

  1. class LocalKeyDemo extends StatefulWidget {
  2. @override
  3. _LocalKeyDemoState createState() => _LocalKeyDemoState();
  4. }
  5. class _LocalKeyDemoState extends State<LocalKeyDemo> {
  6. List<TitleItem> _items = [
  7. TitleItem(title: "aaaaa"),
  8. TitleItem(title: "bbbbb"),
  9. TitleItem(title: "ccccc"),
  10. ];
  11. @override
  12. Widget build(BuildContext context) {
  13. return Container(
  14. child: Scaffold(
  15. appBar: AppBar(
  16. title: Text("Key"),
  17. ),
  18. body: Row(
  19. mainAxisAlignment: MainAxisAlignment.center,
  20. children: _items,
  21. ),
  22. floatingActionButton: FloatingActionButton(
  23. onPressed: (){
  24. setState(() {
  25. _items.removeAt(0);
  26. });
  27. },
  28. child: Icon(Icons.delete),
  29. ),
  30. ),
  31. );
  32. }
  33. }
  34. class TitleItem extends StatefulWidget {
  35. final String title;
  36. TitleItem({this.title});
  37. @override
  38. _TitleItemState createState() => _TitleItemState();
  39. }
  40. class _TitleItemState extends State<TitleItem> {
  41. final Color _randomColor = Color.fromRGBO(Random().nextInt(256), Random().nextInt(256), Random().nextInt(256), 1);
  42. @override
  43. Widget build(BuildContext context) {
  44. return Container(
  45. color: _randomColor,
  46. width: 100,
  47. height: 100,
  48. child: Center(
  49. child: Text(widget.title),
  50. ),
  51. );
  52. }
  53. }

上述代码 创建了3个颜色不同且不同文本的子widget,按照从左往右排列,要注意的是,颜色对象保存在State中,而文本对象保存在Widget中,然后点击按钮,每次删除最左边的widget。
执行结果:
image.png

虽然每次都能删除文本正确的widget,但是颜色值下一个widget的颜色

这种奇怪现象的原因其实和Flutter的增量渲染机制有关:
我们知道widget树是对应着element树的,而widget树是不稳定的,widget在每次的刷新中都有可能在创建或者销毁,而element不会,他会去对比树中新的widget和旧widget是否一致,是否可以更新widget,如果可以更新,那么element将会指向新的widget。如下图:

image.pngimage.png

一开始,widget树的节点与element树的节点是一一对应的,element的_widget属性指向了对应的widget,当widget树发生改变

image.png
element树并不会全部重新创建,而是与从左到右比较,看是否新的节点的widget与原先的节点的widget是否一致,而判断的依据我们可以在源码中看到

  1. static bool canUpdate(Widget oldWidget, Widget newWidget) {
  2. return oldWidget.runtimeType == newWidget.runtimeType
  3. && oldWidget.key == newWidget.key;
  4. }

他比较的新旧widget的runtimeType和key值,明显widgetA和widgetB的runtimeType和key均一致,所以element会变成这样

image.png
element会复用,而widget则换成新的widget,当state并没有改变,而在案例中颜色是存放在sate中的,所以,首个element的文本变成了widgetB的文本,但是颜色还是sateA的的颜色,而widgetC的颜色变成了stateB的颜色,而由于原先3个节点变成两个,所以最后一个element被移除,最终变成我们看到的结果。

要解决这个问题,我们只需要在Flutter判断是否更新widget的时候给一个false的结果,就能解决这个问题,两个对比条件中,runtimeType肯定是一致的,所以,可以给widget添加一个唯一标示key
我们只需要对代码做下面的修改

  1. class _LocalKeyDemoState extends State<LocalKeyDemo> {
  2. List<TitleItem> _items = [
  3. TitleItem(title: "aaaaa", key: ValueKey(1),),
  4. TitleItem(title: "bbbbb", key: ValueKey(2),),
  5. TitleItem(title: "ccccc", key: ValueKey(3),),
  6. ];
  7. }
  8. class TitleItem extends StatefulWidget {
  9. final String title;
  10. TitleItem({this.title, Key key}) : super(key: key);
  11. @override
  12. _TitleItemState createState() => _TitleItemState();
  13. }

这样,结果就是正常的了。

final Color _randomColor变量定义在StatefulWidget中结果也是正常的,由于Widget持有_randomColor,所有复用Element在改变Widget时,颜色也会随Widget一起编码

LocalKey可以给Widget作为唯一表示,在element树更新能准确的更新。

GlobalKey

GlobalKey可以获取到对应的State,Element以及Widget,源码如下:

  1. BuildContext get currentContext => _currentElement;
  2. Widget get currentWidget => _currentElement?.widget;
  3. T get currentState {
  4. final Element element = _currentElement;
  5. if (element is StatefulElement) {
  6. final StatefulElement statefulElement = element;
  7. final State state = statefulElement.state;
  8. if (state is T)
  9. return state;
  10. }
  11. return null;
  12. }
  13. }

利用这个特性,可以实现局部刷新从而进行优化:
如果只是根widget的按钮被点击,而需要改变的仅仅是子widget,我们并不需要刷新整个widget树,
根Widget通过GlobalKey拿到对应子Widget的State,仅刷新子widget的状态,从而优化性能。

  1. class GlobalKeyDemo extends StatelessWidget {
  2. final GlobalKey<_ChildPageState> _globalKey = GlobalKey();
  3. @override
  4. Widget build(BuildContext context) {
  5. return Container(
  6. child: Scaffold(
  7. appBar: AppBar(
  8. title: Text("GlobalKey"),
  9. ),
  10. body: ChildPage(key: _globalKey),
  11. floatingActionButton: FloatingActionButton(
  12. onPressed: () {
  13. _globalKey.currentState.setState(() {
  14. _globalKey.currentState.count ++;
  15. });
  16. },
  17. child: Icon(Icons.add),
  18. ),
  19. ),
  20. );
  21. }
  22. }
  23. class ChildPage extends StatefulWidget {
  24. ChildPage({Key key}):super(key: key);
  25. @override
  26. _ChildPageState createState() => _ChildPageState();
  27. }
  28. class _ChildPageState extends State<ChildPage> {
  29. int count = 0;
  30. @override
  31. Widget build(BuildContext context) {
  32. return Container(
  33. child: Center(
  34. child: Text(count.toString(), style: TextStyle(fontSize: 30, fontWeight: FontWeight.w800, color: Colors.blue),),
  35. ),
  36. );
  37. }
  38. }

总结

Key是一个抽象类,有LocalKey和GlobalKey两个子类:
LocalKey可以作为Widget的唯一标示,避免Element的重用,在删除和排序中创建Widget需加入Key
而GlobalKey可以拿到指定的Widget、Element、State。