Flutter中的Key(一)

1,914 阅读4分钟

本篇主要通过一些实例来深入分析Flutter中的key

简介

通过key.dart中的注释可以看到相关说明

  • 它是 Widgets, Elements and SemanticsNodes 的标识符
  • 当新Widget的Key和Element相关联的当前Widget的Key相等时,才会将Element关联的Widget更新成最新的Widget
  • 具有相同Parent的Elements,key必须唯一
  • 它有两个子类 LocalKey和GlobalKey
  • 推荐 www.youtube.com/watch?v=kn0… 

Flutter中的内部重建机制,有时候需要配合Key的使用才能触发真正的“重建”,key通常在widget的构造函数中,当widget在widget树中移动时,Keys存储对应的state,在实际中,这将能帮助我们存储用户滑动的位置,修改widget集合等等

什么是key

大多数时候我们并不需要key,但是当我们需要对具有某些状态且相同类型的组件 进行  添加、移除、或者重排序时,那就需要使用key,否则就会遇到一些古怪的问题,看下例子,
点击界面上的一个按钮,然后交换行中的两个色块
**

StatelessWidget 实现

使用 StatelessWidget(StatelessColorfulTile) 做 child(tiles):

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

image.png

StatefulWidget 实现

使用 StatefulWidget(StatefulColorfulTile) 做 child(tiles):

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

结果点击切换颜色按钮,没有反应了
image.png

为了解决这个问题,我们在StatefulColorfulTile widget构造时传入一个UniqueKey

class _ScreenState extends State<Screen> {
  List<Widget> widgets = [
    StatefulContainer(key: UniqueKey(),),
    StatefulContainer(key: UniqueKey(),),
  ];
  ···

然后点击切换按钮,又可以愉快地交换颜色了。

为什么StatelessWidget正常更新,StatefullWidget就更新失效,加了key之后又可以了呢?为了弄清楚这其中发生了什么,我们需要再次弄清楚Flutter中widget的更新原理
在framework.dart中可以看到 关于widget的代码

@immutable
abstract class Widget extends DiagnosticableTree {
  const Widget({ this.key });
  final Key key;
  ···
    
  /// 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;
  }
}

由于widget只是一个无法修改的配置,而Element才是真正被修改使用的对象,在前面的文章可以知道,当新的widget到来时将会调用canUpdate方法来确定这个Element是否需要更新,从上面可以看出,canUpdate 对两个(新老) Widget 的 runtimeType 和 key 进行比较,从而判断出当前的** Element 是否需要更新**。

  • StatelessContainer比较

我们并没有传入key,所以只比较两个runtimeType,我们将color定义在widget中,这将使得他们具有不同的runtimeType,因此能够更新element 显示出交换位置的效果

1_sHDIVXBu9RpJYN9Zdn8iBw.gif

  • StatefulContainer比较过程

改成stateful之后 我们将color的定义放在在State中,Widget并不保存State,真正hold State的引用是Stateful Element,在我们没有给widget设置key之前,将只会比较这两个widget的runtimeType,由于两个widget的属性和方法都相同,canUpdate方法将返回false,在Flutter看来,没有发生变化,因此点击按钮 色块并没有交换,当我们给widget一个key以后,canUpdate方法将会比较两个widget的runtimeType以及key,返回true(这里runtimeType相同,key不同),这样就可以正确感知两个widget交换顺序,但是这种比较也是有范围的tu

如何正确设置key

为了提升性能,Flutter的diff算法是有范围的,会对某一个层级的widget进行比较而不是一个个比较,我们把上面的ok的例子再改动一下,将带key的 StatefulContainer 包裹上 Padding 组件,然后点击交换按钮,会发生下面奇怪的现象。点击之后不是交换widget,而是重新创建了!

@override
void initState() {
  super.initState();
  tiles = [
    Padding(
      padding: const EdgeInsets.all(8.0),
      child: StatefulColorfulTile(key: UniqueKey()),
    ),
    Padding(
      padding: const EdgeInsets.all(8.0),
      child: StatefulColorfulTile(key: UniqueKey()),
    ),
  ];
}

对应的widget和Element树如下

image.png

1_uC-SRZpRkOZCEr_rGisF9g.gif
为什么会出现这种问题,上面提到,Flutter 的 Elemetn to Widget 匹配算法将一次只检查树的一个层级:,
(1)显然Padding并没有发生本质的变化
image.png

(2)于是开始第二层的对比,此时发现元素与组件的Key并不匹配,于是把它设置成不可用状态,但是这里的key是本地key,(Local Key),Flutter并不能找到另一层里面的Key(另外一个Padding Widget中的key),因此flutter就创建了一个新的
1_uC-SRZpRkOZCEr_rGisF9g.gif
因此为了解决这个问题,我们需要将key放到Row的children这一层

class _ScreenState extends State<Screen> {
  List<Widget> widgets = [
    Padding(
      key: UniqueKey(),
      padding: const EdgeInsets.all(8.0),
      child: StatefulContainer(),
    ),
    Padding(
      key: UniqueKey(),
      padding: const EdgeInsets.all(8.0),
      child: StatefulContainer(),
    ),
  ];

参考

扫码_搜索联合传播样式-白色版.png