Flutter中key的作用

概述

在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_initial.png

widget_first.png
widget_second.png
widget_last.png

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

widget_tree_1.png

element_tree_1.png-w400

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

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


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<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。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342