二、Flutter中Key的作用

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,),),
      ),
    );
  }
}

但是结果却跟我们想要实现的功能不一样,如下图展示:


Mar-23-2021 10-06-00.gif

点击按钮确实按照A,B,C,D的顺序被删除了,但是颜色却是按着D,C,B,A的顺序被删了,为什么会出现这个奇怪的问题呢?

我们来尝试解决这个问题。

  • 方案一
    Item这个WidgetStatefulWidget改为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,),),
      ),
    );
  }
}

这次重现象上看是正常了,满足了需求:


Mar-23-2021 10-38-10.gif
  • 方案二
    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,
          ),
        ),
      ),
    );
  }
}

看一下结果:


Mar-23-2021 10-48-39.gif

也是满足需求的。

来分析一下原因。

首先要明确一点,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树的关系如下:


    image.png

Element树中的elementWidget树中的widget是一一对应的,element对象持有widgetstate两个对象,Itemtitle保存在widget中,而_color保存state中,如上图展示。当我们删除了第一个Item-AAAAAA之后,重新build时候这时候由于AAAAAA被删除了,BBBBBBIndex就变成了0Element树就会拿Index0widgetWidget树的index0widget比较,看看是否更新widget,比较的内容就是runtimeTypekey,由于AAAAAABBBBBB的类型都是Item,并且keynull,所以Element就认为widget没有发生变化,如实就更新了widget,但是state没有发生变化:

image.png

同理接着 build CCCCCC的时候Element会拿Index为1的element里面的widgetWidget树里Index1Element作比较,同样是可以更新的,如是就更新了widget但是state仍然没变:

image.png

同理 build DDDDDD

image.png

buildDDDDDD之后,Element发现有多的element没有widget与之对应,就把多的element对象干掉了:

image.png

上面就解释了为什么我们看到的是Title被正常删除了,但是颜色却是从后往前删的原因了。

那为什么方案一方案二都可以解决这个问题呢,不管是方案一还是方案二都是将颜色也放到了elementwidget中了,这样在更新widget的时候,titlecolor就一起更新了。

产生这个问题的根本原因是在更新的时候新旧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树为:

image.png

当删除AAAAAA之后,重新构建BBBBBB的时候:

image.png

widget还是会取element的树index0widget来和自己比较看是否更新,这时key是不一致的,所以element就会被干掉重新生成了:

image.png

生成CCCCCCDDDDDD是同样的道理,这里就不展示了。这样就可以实现需求了。

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();
}

它的直接子类型有两个LocalKeyGlobalKey两种。

2.1 LocalKey

LocalKeydiff算法的核心所在,用做ElementWidget的比较。常用子类有以下几个:

  • ValueKey:以一个数据为作为key,比如数字、字符等。
  • ObjectKey:以Object对象作为Key
  • UniqueKey:可以保证key的唯一性,如果使用这个类型的key,那么Element对象将不会被复用。
  • PageStorageKey:用于存储页面滚动位置的key

2.2 GlobalKey

每个globalkey都是一个在整个应用内唯一的keyglobalkey相对而言是比较昂贵的,如果你并不需要globalkey的某些特性,那么可以考虑使用KeyValueKeyObjectKeyUniqueKey
他有两个用途:

  • 用途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,
          ),
        ),
      ]),
    );
  }
}
Mar-23-2021 16-52-34.gif

这个例子比较简单,展示通过GlobalKey获取子WidgetState并更新。

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

推荐阅读更多精彩内容

  • 前言 在开发 Flutter 的过程中你可能会发现,一些小部件的构造函数中都有一个可选的参数——Key。刚接触的同...
    Vadaski阅读 6,837评论 15 53
  • 1.setState的原理 setstate会触发canUpdate函数的调用,这个函数会对比old widget...
    辣条少年J阅读 1,862评论 0 1
  • 概述 在Widget的构造方法中,有Key这么一个可选参数,Key是一个抽象类,有LocalKey和GlobalK...
    iOSer_jia阅读 1,514评论 0 5
  • 文章来源于Flutter Widgets 101 Ep. 4,感兴趣的同学可以直接看视频,更便于理解。 首先,我们...
    森码阅读 18,861评论 7 37
  • 新创建一个Flutter Application的时候,默认生成的代码里面有这么一段 title很好理解,给App...
    whqfor阅读 860评论 0 0