Flutter - Key

原文在这里。这篇文章是油管视频的总结。视频地址是这里git

基本上每一个widget都有key参数,可是使用的方法确各有不一样。在widget从widget树的一个地方移动到另外一个地方的时候,key会保存状态。在实际使用中,Key能够用来保存用户滚动的位置或者保存集合修改的状态。github

Key的内部原理

大部分时间用不到Key。加了也不会有什么反作用,不过也不必消耗额外的空间。就像这样Map<Foo, Bar> aMap = Map<Foo, Bar>();初始化了一个变量扔着同样。可是,若是你要对一个同类型,有状态的widget集合添加、删除或者排序,那就要Key的的参与了app

为了说明为何你修改一个widget集合的时候须要用到key,我(做者)写了一个简单的例子。这个例子里面有两个widget,随机显示颜色。当你点击里面的一个按钮的时候,这两个组件会互换位置。:less

在无状态版本里面,有两个无状态的组件分别显示随机颜色。这个两个无状态的widget包含在一个叫作PositionedTiles的有状态的wiget里。两个显示颜色的widget的位置也保存在里面。当FloatingActionButton被点击的时候,两个无状态颜色组件就会交换位置。代码以下:ide

void main() => runApp(new MaterialApp(home: PositionedTiles()));

class PositionedTiles extends StatefulWidget {
 @override
 State<StatefulWidget> createState() => PositionedTilesState();
}

class PositionedTilesState extends State<PositionedTiles> {
 List<Widget> tiles = [
   StatelessColorfulTile(),
   StatelessColorfulTile(),
 ];

 @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 StatelessColorfulTile extends StatelessWidget {
 Color myColor = UniqueColorGenerator.getColor();
 @override
 Widget build(BuildContext context) {
   return Container(
       color: myColor, child: Padding(padding: EdgeInsets.all(70.0)));
 }
}

可是,若是我让ColorfulTiles变成有状态的,颜色都保存在状态里,当我点击按钮的时候,看起来什么都不会发生。测试

List<Widget> tiles = [
   StatefulColorfulTile(),
   StatefulColorfulTile(),
];

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

可是,这个代码是有bug,点了“交换”按钮的时候,两个颜色的widget不会交换。只有在颜色widget里面加上key参数才能够达到这个效果。动画

List<Widget> tiles = [
  StatefulColorfulTile(key: UniqueKey()), // Keys added here
  StatefulColorfulTile(key: UniqueKey()),
];

...
class StatefulColorfulTile extends StatefulWidget {
  StatefulColorfulTile({Key key}) : super(key: key);  // NEW CONSTRUCTOR
 
  @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),
        ));
  }
}

可是,只有在修改有状态的子树的时候才是必须的。若是整个子树的widget集合都是无状态的,那么Key并非必须的。ui

这些就是在Flutter里使用Key所须要知道的所有了。固然,若是你要知道这里面的原理的话,请继续往下看。。。spa

为何Key有的时候是必须的

如你所知,每一个widget都有一个对应的element。就如同构建一个widget树同样,Flutter也会构建一个对应的Element树。这个ElementTree很是简单,只保存了每一个widget的类型和子element。你能够认为element树是Flutter app的骨架、蓝图。任何其余的信息均可以从element找到对应的widget而后拿到。3d

在上例的Row widget里保存了一个有序的子节点列表。当咱们交换Row里颜色widget的顺序的时候,Flutter会遍历ElementTree,对比交换先后树的结构是否发生了改变。

Flutter从RowElement开始,而后移动子节点。ElementTree检查新的widget类型和key与旧的节点是否有不一样。若是有不一样,它会把引用指向新的widget。在无状态版本里,widget并无key,因此Flutter只是检查了类型。若是这样看起来信息量太大的话,能够直接看上面的动图。

在element树种,对有状态widget的处理略微不一样。仍是会有上文说到的widget和element,不过也会有保存状态的对象。颜色久保存在这些状态里,而不是widget里。

在有状态,没有key的例子里,当交换widget的按钮按下的时候,Flutter会遍历ElementTree,检查Row的类型,以后更新引用。以后是颜色element,检查颜色widget是否为一样的类型,并更新引用。由于Flutter使用了ElementTree和它的state来决定什么东西能够显示在你的设备上。从用户的角度看,两个颜色widget并无正确的互换。

在上面问题的修改版中,颜色widget里面多了一个key参数。如今再点击交换颜色的按钮的时候, Row widget仍是和以前同样,可是两个颜色的element的key和widget的key是不一样的,这样会致使Flutter在Row element从第一个key值不匹配的地方开始重构element子树。

以后Flutter会在Row子节点里找到key值匹配的element来重构子树。找到一个key值匹配的就更新它对widget的引用。知道整个element子树重构完成。这样Flutter就能够正确的显示颜色交换了。

言而言之,若是要修改一列状态widget的数量、顺序的时候Key就必不可少了。为了强调,在本例中颜色的值存在了state里。state存在有点时候很微小、不起眼,在动画,用户输入数据的显示和滚动的位置等地方都会用到。

Key放在哪

基本上,若是要在app里使用key的话,那么就应该放在存放state的widget子树的最顶端
一个常常会犯的错误是,不少人会把key放在第一个状态widget里面,可是这样是不对的。不信?来稍微修改一下上面的例子。如今Padding widget包在了颜色widget的外面,可是key仍是放在颜色widget上面。

void main() => runApp(new MaterialApp(home: PositionedTiles()));

class PositionedTiles extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => PositionedTilesState();
}

class PositionedTilesState extends State<PositionedTiles> {
  // Stateful tiles now wrapped in padding (a stateless widget) to increase height 
  // of widget tree and show why keys are needed at the Padding level.
  List<Widget> tiles = [
    Padding(
      padding: const EdgeInsets.all(8.0),
      child: StatefulColorfulTile(key: UniqueKey()),
    ),
    Padding(
      padding: const EdgeInsets.all(8.0),
      child: StatefulColorfulTile(key: UniqueKey()),
    ),
  ];

  @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 {
  StatefulColorfulTile({Key key}) : super(key: key);
 
  @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),
        ));
  }
}

点击交换按钮以后,两个颜色组件显示出了彻底不一样的颜色。

这是对应的element树的样子:


当咱们交换两个子widget的位置以后,Flutter里element到widget的检查机制每次只会检查element树的一层。下图把叶子节点都灰化处理了,这样咱们能够更加注意到底发生了什么。在Padding widget这一层,全部运做都是正确的。

在第二层,Flutter会发现颜色widget的key和element的key不匹配,它会移除掉这些element的引用。本例中使用的是LocalKeys。也就是说在element的对比中,Flutter只会查看树的某个范围内对比key的值是否匹配。

由于在这个范围内找不到匹配key值的element,那么它就会建立一个新的,因此会初始化一个新的状态。因此在本例中,widget显示的颜色是从新生成的随机色。


那么,若是在Padding widget上面加上key值呢?

void main() => runApp(new MaterialApp(home: PositionedTiles()));

class PositionedTiles extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => PositionedTilesState();
}

class PositionedTilesState extends State<PositionedTiles> {
  List<Widget> tiles = [
    Padding(
      // Place the keys at the *top* of the tree of the items in the collection.
      key: UniqueKey(), 
      padding: const EdgeInsets.all(8.0),
      child: StatefulColorfulTile(),
    ),
    Padding(
      key: UniqueKey(),
      padding: const EdgeInsets.all(8.0),
      child: 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 {
  StatefulColorfulTile({Key key}) : super(key: key);
 
  @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),
        ));
  }
}

Flutter注意到了问题,并会正确的更新。

我应该用哪一种Key

咱们要用的key的类型主要看widget要作到什么特色的区分。下面要介绍四种key:ValueKey, ObjectKey, UniqueKeyPageStorageKey, GlobalKey.

ValueKey

好比下面的todo app,你能够对各条目从新排序。

在这个场景下,若是一个条目的文本能够认为是一个常量,而且是惟一的,那么就是用于ValueKey。文本就是“value”值。

return TodoItem(
  key: ValueKey(todo.task),
  todo: todo,
  onDismissed: (direction) => _removeTodo(context, todo),
);

Objectkey

另外的一个场景,好比你有一个地址簿app。里面保存了不一样人的信息。在这个状况下,每一个widget都保存了一个复杂的数据。每一个单独的字段,好比姓名或者出生日期均可能和其余的数据是同样,可是这些数据组合起来就是惟一的。那么,这就很实用于ObjectKey

Uniquekey

若是多个widget有一样的值,或者你想要确保每一个widget都不一样,那么就可使用UniqueKey。上面的例子中就使用了UniqueKey,由于在颜色widget里面并无其余的值能够区分于其余的widget了。使用UniqueKey要当心。若是你在build方法里建立了一个新的UniqueKey,那么这个widget每次调用build方法以后都会获得一个不一样的UniqueKey这样就把key的好处所有的抹煞了。

相似的,千万不要考虑使用随机数来做为你的key。每次一个widget调用了build方法就会生成一个随机数,那么多个帧的连续性也就被破坏了。那么,效果也就和一开始就没用key的效果是同样的了。

PageStoragekey

这是一个很特殊的key,它保存了用户滚动的位置,这样app能够保存用户滚动的位置给下次用户打开的时候直接到上次滚动的位置。

GlobalKey

有两个用处:

  • 能够在app的任何地方更换父widget而不会丢失状态
  • 它能够用来从彻底不一样的widget树里面访问数据

第一种状况的一个例子是若是你要在不一样的地方显示同一个widget,并且state也是相同的,那么GlobalKey就是最好的选择。

第二种状况,若是你想要验证一个密码,可是又不想在不一样的widget之间共享状态。

GlobalKey也能够用于测试,使用一个key来访问某个特定的widget,而后查看里面的数据。

一般(并非所有),GlobalKey更像是一个全局变量。老是有其余的方法能够访问state,好比InheritedWidget,或者相似Redux的库或者BLoC模式的实现。

总结

总而言之,在widget树里面保存state就要考虑用key了。通常要修改一列一样类型的widget的时候,好比一个列表。把key值放在要保存state的子树的顶层widget上。根据要展示的数据和使用的场景选择合适的key类型。

Todo app的代码在这里

相关文章
相关标签/搜索