Flutter中key的作用

概述

在Widget的构造方法中,有Key这么一个可选参数,Key是一个抽象类,有LocalKey和GlobalKey两种,本文将对这两种key的作用进行探究。

LocalKey

在探究LocalKey的作用之前,先通过一段代码来看看一个场景

class LocalKeyDemo extends StatefulWidget {
  @override
  _LocalKeyDemoState createState() => _LocalKeyDemoState();
}

class _LocalKeyDemoState extends State {
  List _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 {
  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。

执行结果出乎意料:


Flutter中key的作用_第1张图片
widget_initial.png

Flutter中key的作用_第2张图片
widget_first.png
Flutter中key的作用_第3张图片
widget_second.png
Flutter中key的作用_第4张图片
widget_last.png

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

Flutter中key的作用_第5张图片
widget_tree_1.png

Flutter中key的作用_第6张图片
element_tree_1.png-w400

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

Flutter中key的作用_第7张图片
widget_tree_2.png

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会变成这样


Flutter中key的作用_第8张图片
element_tree_2.png

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

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

我们只需要对代码做下面的修改

...
class _LocalKeyDemoState extends State {
  List _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 {
  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。

你可能感兴趣的:(Flutter中key的作用)