Flutter中的的key是什么,有什么用

Key的定义

先看下Key的官方定义:

A Key is an identifier for Widgets, Elements and SemanticsNodes.
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.
www.youtube.com/watch?v=kn0EOS-ZiIc 
Keys must be unique amongst the Elements with the same parent.
Subclasses of Key should either subclass LocalKey or GlobalKey.


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.
  
  const Key.empty();
}

从中提取到关于Key的3个关键信息:

1,Key是Widgets、Elements、SemanticsNodes的标识符。
2,如果新部件的键与与该元素相关联的当前部件的键相同,则新部件才用于更新现有元素。
3,在父元素相同的元素中,key必须是唯一的。

Key 派生出两种不同用途的Key:LocalKey 和 GlobalKey。Key的子类应该是LocalKey或GlobalKey的子类。

  • Localkey

LocalKey 直接继承至 Key,它应用于拥有相同父 widget 的小部件进行比较的情况,比如一个widget有多个子 Widget,需要对它的子 widget 进行移动处理时,应该使用Localkey

Localkey 派生出了许多子类 key:

  • ValueKey : ValueKey('String')
  • ObjectKey : ObjectKey(Object)
  • UniqueKey : UniqueKey()

Valuekey 又派生出了 PageStorageKey

  • GlobalKey

abstract class GlobalKey<T extends State<StatefulWidget>> extends Key {
    
    
  /// Creates a [LabeledGlobalKey], which is a [GlobalKey] with a label used for
  /// debugging.
  ///
  /// The label is purely for debugging and not used for comparing the identity
  /// of the key.
  factory GlobalKey({
    
     String? debugLabel }) => LabeledGlobalKey<T>(debugLabel);

  /// Creates a global key without a label.
  ///
  /// Used by subclasses because the factory constructor shadows the implicit
  /// constructor.
  const GlobalKey.constructor() : super.empty();

  Element? get _currentElement => WidgetsBinding.instance!.buildOwner!._globalKeyRegistry[this];

  /// The build context in which the widget with this key builds.
  ///
  /// The current context is null if there is no widget in the tree that matches
  /// this global key.
  BuildContext? get currentContext => _currentElement;

  /// The widget in the tree that currently has this global key.
  ///
  /// The current widget is null if there is no widget in the tree that matches
  /// this global key.
  Widget? get currentWidget => _currentElement?.widget;

  /// The [State] for the widget in the tree that currently has this global key.
  ///
  /// The current state is null if (1) there is no widget in the tree that
  /// matches this global key, (2) that widget is not a [StatefulWidget], or the
  /// associated [State] object is not a subtype of `T`.
  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;
  }
}

你可以通过 GlobalKey 找到持有该GlobalKey的 Widget 、State、 Element。

在父元素相同的元素中,键必须是唯一的。相比之下,GlobalKey在整个应用程序中必须是唯一的

注意:GlobalKey 是非常昂贵的,需要谨慎使用。

widget 的 diff 更新机制

Widget 可以有 Stateful 和 Stateless 两种,两种widget构造函数中都有一个可选的参数Key,key是widget的标识符且能够帮助开发者在 Widget tree 中保存状态。

下面我们通过一个demo详细说明Key对于widget的作用机制

class StatelessDemo extends StatelessWidget {
    
    
  final randomValue = Random().nextInt(10000);
  
  Widget build(BuildContext context) {
    
    
    // TODO: implement build
    return Text('$randomValue');
  }
}

这是一个很简单的 Stateless Widget,在界面上显示一个随机数。 Random().nextInt(10000) 能够为这个 Widget 初始化一个小于10000的随机数。

将这个Widget展示到界面上:

class MyHomePage extends StatefulWidget {
    
    
  
  State<StatefulWidget> createState() {
    
    
    // TODO: implement createState
    return _MyHomePageState();
  }
}

class _MyHomePageState extends State<MyHomePage> {
    
    
  List<StatelessDemo> widgetArr = [StatelessDemo(), StatelessDemo()];

  
  Widget build(BuildContext context) {
    
    
    // TODO: implement build
    return Padding(
      padding: EdgeInsets.only(top: 100),
      child: Column(
        children: [
          widgetArr[0],
          SizedBox(height: 50),
          widgetArr[1],
          SizedBox(height: 80),
          TextButton(
              onPressed: () {
    
    
                setState(() {
    
    
                  widgetArr.insert(0, widgetArr.removeAt(1));
                });
              },
              child: Text('交换widget位置'))
        ],
      ),
    );
  }
}

在界面展示了两个 StatelessDemo组件,当我们点击 TextButton 时,将会执行交换它们的顺序的操作。

现在我们做一点小小的改动,将这个 StatelessDemo 升级为 StatefulDemo:

class StatefulDemo extends StatefulWidget {
    
    
  
  State<StatefulWidget> createState() {
    
    
    // TODO: implement createState
    return StatefulDemoState();
  }
}

class StatefulDemoState extends State<StatefulDemo> {
    
    
  final randomValue = Random().nextInt(10000);
  
  
  Widget build(BuildContext context) {
    
    
    // TODO: implement build
    return Text('$randomValue');
  }
}

在 StatefulDemo 中,我们将定义 Random 和 build 方法都放进了 State 中。

现在我们还是使用刚才一样的布局,只不过把 StatelessDemo 替换成 StatefulDemo,看看会发生什么。

这时,无论我们怎样点击,都再也没有办法交换这两个widget的顺序了,而 setState 确实是被执行了的。

为了解决这个问题,我们在两个 Widget 构造的时候给它传入一个 UniqueKey:

class _MyHomePageState extends State<MyHomePage> {
    
    
  List<StatefulDemo> widgetArr = [
    StatefulDemo(key: UniqueKey()),
    StatefulDemo(key: UniqueKey())
  ];
  
  、、、、、、、、、
}

然后这两个 Widget 又可以正常被交换顺序了。

为什么 Stateful Widget 无法正常交换顺序,加上了 Key 之后就可以了,在这之中到底发生了什么? 为了弄明白这个问题,我们将涉及 Widget 的 diff 更新机制

/// 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 到来时将会调用 canUpdate 方法,来确定这个 Element是否需要更新,Widget 只是一个配置且无法修改,而 Element 才是真正被使用的对象,并可以被修改。canUpdate 对两个(新老) Widget 的 runtimeType 和 key 进行比较,从而判断出当前的 Element 是否需要更新。若 canUpdate 方法返回 true 说明不需要替换 Element,直接更新 Widget 就可以了。

  • StatelessDemo 比较过程:

在 StatelessDemo 中,我们并没有传入 key ,所以只比较它们的 runtimeType。这里 runtimeType 一致,canUpdate 方法返回 true,两个 Widget 被交换了位置,此时这两个 Element 将不会交换位置,Element 调用新持有 Widget 的 build 方法重新构建,而我们的 randomValue 实际上就是储存在 widget 中的,因此在屏幕上两个 Widget 便被正确的交换了顺序。

  • StatefulDemo 比较过程:

在 StatefulDemo,我们将 randomValue 的定义放在了 State 中,Widget 并不保存 State,真正 hold State 引用的是 Stateful Element

当我们没有给 Widget 任何 key 的时候,将会只比较这两个 Widget 的 runtimeType,由于两个 Widget 的runtimeType相同,canUpdate 方法将会返回 true,于是两个 StatefulWidget 会交换位置,注意此时这两个 Element 将不会交换位置。原有 Element 只会从它持有的 widget 的build 方法重新构建, 由于randomValue 的定义放在了 State 中,所以randomValue不会交换,这里变换 StatefulWidget 的位置是没有作用的,因为randomValue由State持有而不是widget。

当给 Widget 一个 key 之后,canUpdate 方法将会比较两个 Widget 的 runtimeType 以及 key,此时两个Widget的runtimeType相同但key不同,所以 返回false。因为canUpdate返回false,此时不使用当前对应widget进行更新,而是根据当前相对应widget创建新的Element,创建新的Element的话,就会重新创建state,randomValue 的定义放在了 State 中,看起来就像两个element交换了。

什么时候需要使用 Key

  • ValueKey:对列表ListView中item进行滑动删除的时候需要用到

  • ObjectKey:如果你有一个电话本应用,它可以记录某个人的电话号码,并用列表显示出来,同样的还是需要有一个滑动删除操作。

我们知道人名可能会重复,这时候你无法保证给 Key 的值每次都会不同。但是,当人名和电话号码组合起来的 Object 将具有唯一性。

这时候你需要使用 ObjectKey。

  • UniqueKey:如果组合的 Object 都无法满足唯一性的时候,你想要确保每一个 Key 都具有唯一性。那么,你可以使用 UniqueKey。它将会通过该对象生成一个具有唯一性的 hash 码。不过这样做,每次 Widget 被构建时都会去重新生成一个新的 UniqueKey,失去了一致性,也就是说你的小部件还是会改变。

  • PageStorageKey:用于保存页面状态,比如当你有一个滑动列表,你通过某一个 Item 跳转到了一个新的页面,当你返回之前的列表页面时,你发现滑动的距离回到了顶部。这时候,给 Sliver 一个 PageStorageKey!它将能够保持 Sliver 的滚动状态。

  • GlobalKey:GlobalKey 用于跨 Widget 访问状态。

参考

官方视频-讲解Key

猜你喜欢

转载自blog.csdn.net/qq_36162336/article/details/130599383