flutter key 四个例子详解

1,025 阅读7分钟

key有啥用?

绑定widget和element,在widget移动的时候,带上element一起移动。

源码

key源码解释:

///class Key
/// A [Key] is an identifier for [Widget]s, [Element]s and [SemanticsNode]s.
///
/// A new widget will only be used to update an existing element if its key is
/// the same as the key of the current widget associated with the element.
///
/// {@youtube 560 315 <https://www.youtube.com/watch?v=kn0EOS-ZiIc>}
///
/// Keys must be unique amongst the [Element]s with the same parent.
///
/// Subclasses of [Key] should either subclass [LocalKey] or [GlobalKey].
///
/// See also:
///
///  * [Widget.key], which discusses how widgets use keys.

机翻:

key是widget、element、semanticsNode的标识。如果一个key用于绑定一个element和一个widget,那么新的widget只会被用于更新这个已经存在的element。

既然注释这么写了,是不是,不使用key的话,widget和element,就不是绑定关系?

好像,还真的是这样。不绑定,就不会存在关系!(废话

Widget中对于key的源码解释:

	/// class Widget
	/// Controls how one widget replaces another widget in the tree.
  ///
  /// If the [runtimeType] and [key] properties of the two widgets are
  /// [operator==], respectively, then the new widget replaces the old widget by
  /// updating the underlying element (i.e., by calling [Element.update] with the
  /// new widget). Otherwise, the old element is removed from the tree, the new
  /// widget is inflated into an element, and the new element is inserted into the
  /// tree.
  ///
  /// In addition, using a [GlobalKey] as the widget's [key] allows the element
  /// to be moved around the tree (changing parent) without losing state. When a
  /// new widget is found (its key and type do not match a previous widget in
  /// the same location), but there was a widget with that same global key
  /// elsewhere in the tree in the previous frame, then that widget's element is
  /// moved to the new location.
  ///
  /// Generally, a widget that is the only child of another widget does not need
  /// an explicit key.
  ///
  /// See also:
  ///
  ///  * The discussions at [Key] and [GlobalKey].

机翻:

    控制widget在树中替换另一个widget。

    如果widget的`runtimeType``key`分别相等,那么新的widget会通过更新底层的element来替换旧的widget(即调用`Element.update`方法,将新的widget替换进去);否则,会从树中移除旧的element,然后将「用新widget更新过自己的element」放置在该位置。

    特别的,使用GlobalKey的widget,可以在不丢失state的情况下,在树中移动element。当一个新widget(即runtimeType和key两个不能同时相等时)出现,并且前一帧的树中其他位置存在一个同样的key时,这个widget对应的element就会被移动到新的位置。(就是这里!告诉我们使用key,可以让statefulWidget带着自己的state走,即带element走(因为state被element持有!))

整个Widget类中,只有canUpdate方法使用了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是否可以用于更新那个「持有旧widget的element」。当且仅当,两个widget拥有相同的runtimeType和key时,一个element可以使用widget作为它的配置,也可以更换另一份widget作为配置。

最后来看一份Elementupdate方法,里面使用到了Widget.canUpdate(widget, newWidget),当且仅当两个widget的runtimeType和key相等时,element才会更新自己的widget。

	/// Change the widget used to configure this element.
  ///
  /// The framework calls this function when the parent wishes to use a
  /// different widget to configure this element. The new widget is guaranteed
  /// to have the same [runtimeType] as the old widget.
  ///
  /// This function is called only during the "active" lifecycle state.
  @mustCallSuper
  void update(covariant Widget newWidget) {
    // This code is hot when hot reloading, so we try to
    // only call _AssertionError._evaluateAssertion once.
    assert(_lifecycleState == _ElementLifecycle.active
        && widget != null
        && newWidget != null
        && newWidget != widget
        && depth != null
        && Widget.canUpdate(widget, newWidget));
    // This Element was told to update and we can now release all the global key
    // reservations of forgotten children. We cannot do this earlier because the
    // forgotten children still represent global key duplications if the element
    // never updates (the forgotten children are not removed from the tree
    // until the call to update happens)
    assert(() {
      _debugForgottenChildrenWithGlobalKey.forEach(_debugRemoveGlobalKeyReservation);
      _debugForgottenChildrenWithGlobalKey.clear();
      return true;
    }());
    _widget = newWidget;
  }

小结一下:

只有使用key的时候,才能把widget和element链接起来,才能在widget移动的时候,将element关联地移动

看完注释之后,结合四个场景深入了解key的影响:

  1. ColorBox为StatelessWidget,且颜色放在build里面

  2. ColorBox为StatelessWidget,颜色由widget管理

  3. ColorBox为StatefulWidget,count由state管理,不使用key

  4. ColorBox为StatefulWidget,count由state管理,使用key

这四种场景的预期都是点击右下角的加号,两个Box交换位置。

Demo

ColorBox为StatelessWidget,且颜色放在build里面

import 'dart:math';

import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';

class ColorStatelessBox extends StatelessWidget {
  ColorStatelessBox({
    Key key,
  }) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return Container(
      color: Color.fromRGBO(Random().nextInt(256), Random().nextInt(256), Random().nextInt(256), 1),
      width: 100,
      height: 100,
    );
  }
}

class ColorStatefulBox extends StatefulWidget {
  const ColorStatefulBox({Key key, this.color}) : super(key: key);
  final Color color;

  @override
  State<ColorStatefulBox> createState() => _ColorStatefulBoxState();
}

class _ColorStatefulBoxState extends State<ColorStatefulBox> {
  int count = 0;
  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {
        count++;
        setState(() {});
      },
      child: Container(
        color: widget.color,
        width: 100,
        height: 100,
        child: Center(
          child: Text(
            count.toString(),
            style: TextStyle(fontSize: 30),
          ),
        ),
      ),
    );
  }
}

class TestPage extends StatefulWidget {
  @override
  _TestPageState createState() => _TestPageState();
}

class _TestPageState extends State<TestPage> {
  List<Widget> widgetList = [ColorStatelessBox(), ColorStatelessBox()];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text("key test"),
        ),
        body: Container(
          padding: EdgeInsets.all(20),
          child: Column(
            children: [...widgetList],
          ),
        ),
        floatingActionButton: GestureDetector(
          onTap: () {
            widgetList = widgetList.reversed.toList();
            setState(() {});
          },
          child: Icon(
            Icons.add,
          ),
        ));
  }
}

1.png2.png3.png

现象:结果是每点击一次右下角的加号,两个ColorStatelessBox的颜色都会随机一次,体现不出交换两个的位置的结果。

分析:因为我们在setState的时候,Element.performRebuild调用了build函数,随机了color,所以每点击一次,颜色都会随机一次。

ColorBox为StatelessWidget,颜色由widget管理

class ColorStatelessBox extends StatelessWidget {
  ColorStatelessBox({
    Key key,
  }) : super(key: key);

  final Color color = Color.fromRGBO(Random().nextInt(256), Random().nextInt(256), Random().nextInt(256), 1);
  @override
  Widget build(BuildContext context) {
    return Container(
      color: color,
      width: 100,
      height: 100,
    );
  }
}
Untitled.png 2.png

改动:将color交给widget管理,不必每次build都随机生成。

现象:点击加号,可以看到确实交换了两个box的位置。

分析:因为在setState的时候,调用Element.update时,接着判断Widget.canUpdate(widget, newWidget),这里widget和newWidget都是ColorStatelessBox类型,而且都没有key参数,所以Element使用了newWidget更新自己,而这个newWidget,就是被交换顺序之后的widget,所以能看到前后两个box确实换了位置。

1.png 2.png

图片来源于参考链接中的视频,强烈推荐观看文末的参考视频。(这里widget tree中红蓝结点分别代表储存在widget中的color颜色)

ColorBox为StatefulWidget,count由state管理,不使用key

1.png 2.png

改动:ColorBox改为StatefulWidget,新增count,储存在state中

现象:两个box初始化时,分别传入yellow、blue作为颜色,点击box改变count,最后点击加号,只有颜色改变了。

分析:首先颜色的改变,同「ColorBox为StatelessWidget,颜色由widget管理」的分析。「color数据是跟着widget的」,但「count数据是跟着state的」,而「element是由widget生成,并且持有state的」。所以颜色换位置了,但是count,即state并没有换位置。我们回到上面Widget源码的第二段,因为两个box都没传key,所以仅匹配runtimeType就已经相等了,所以没有更新element tree。

ColorBox为StatefulWidget,count由state管理,使用key

List<Widget> widgetList = [
    ColorStatefulBox(
      color: Colors.yellow,
      key: GlobalKey(),
    ),
    ColorStatefulBox(
      color: Colors.blue,
      key: GlobalKey(),
    )
  ];
3.png

改动:增加key字段

现象:两个box位置,state都一起换位了

分析:widget更换位置跟前面的分析一样,但这次element的位置也变了。原因还是Widget源码的第二段。增加了key字段之后,原本element就匹配不上换位之后的widget了,就会移除当前位置的element。再根据Widget源码的第三段,找回那个原本key对应的element,将它放到这个位置,那么,就能实现带着state移动的效果。

2.png

(这里她把颜色存在state里面,对应本例中的count)

什么场景需要使用key?

对相同类型、带状态的组件集合,进行add、remove、sort的时候。

其实经过上面四个例子的分析,可以发现,出问题的状况为:多个同类型的StatefulWidget、state保存了数据、组件位置会变化。

常见的场景如:支付宝主页的编辑入口、待办功能checkList。

留个问题,关于widget源码注释中提到的element的移除操作的方法源码我没找到,大家知道的话,可以跟我同步一下。

有问题的话,感谢指出。

参考:www.youtube.com/watch?v=kn0…