Flutter中的Key(二)

3,102 阅读5分钟

本文继续分析flutter中各种各样的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();
}

默认的key会通过工厂方法传入的key 创建一个ValueKey,Key派生出两种用途不同的的key:LocalKey和GlobalKey,类图如下

image.png

GlobalKey

  • GlobalKey唯一标识Elements,它提供了与Element相关联的访问,如BuildContext、State(对于StatefulWidget)
  • 不要在两个Widget中使用相同的GlobalKey
  • Global keys 是很昂贵的,如果你不需要访问BuildContext、Element、State这些的话,请尽量使用[Key], [ValueKey], [ObjectKey] 或者 [UniqueKey]
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();

  static final Map<GlobalKey, Element> _registry = <GlobalKey, Element>{};

  void _register(Element element) {
    _registry[this] = element;
  }

  void _unregister(Element element) {
    if (_registry[this] == element)
      _registry.remove(this);
  }

  Element get _currentElement => _registry[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 currentContext => _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 使用了一个静态常量 Map 来保存它对应的 Element。你可以通过 GlobalKey 找到持有该GlobalKey的 Widget,State 和 Element。它们允许widget在应用中的任何位置更改父级而不会丢失State,内部有几个属性,意味着你可以访问到currentContext、currentContext和currentState(如果是StatefullWidget),这是通过过在Element被mount到树上的时候调用_register,如果是类型是GlobalKey,那么Element就会加入到一个静态Map里,unmount的时候调用_unregister被移除

  • 比如在不同的屏幕上使用相同的Widget,但是保持相同的State,则需要使用GlobalKeys

1_JIPjn-gM6OIG_TfPJvtuVA.gif

class SwitcherScreenState extends State<SwitcherScreen> {
  bool isActive = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Switch.adaptive(
            value: isActive,
            onChanged: (bool currentStatus) {
              isActive = currentStatus;
              setState(() {});
            }),
      ),
    );
  }

  changeState() {
    isActive = !isActive;
    setState(() {});
  }
}

但是我们想要在外部改变该状态,这时候就需要使用 GlobalKey

class _ScreenState extends State<Screen> {
  final GlobalKey<SwitcherScreenState> key = GlobalKey<SwitcherScreenState>();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SwitcherScreen(
        key: key,
      ),
      floatingActionButton: FloatingActionButton(onPressed: () {
        key.currentState.changeState();
      }),
    );
  }
}

通常,GlobalKey看起来有点像全局变量。也有其他更好的办法去查找状态,比如 InheritedWidget、Redux 或 Block Pattern。

Localkey

LocalKey 直接继承至 Key,它应用于拥有相同父 Element 的小部件进行比较的情况,也就是上述例子中,有一个多子 Widget 中需要对它的子 widget 进行移动处理这时候你应该使用Localkey。
Localkey 派生出了许多子类 key:

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

    Valuekey 又派生出了 PageStorageKey : PageStorageKey('value')

  • ValueKey
class ValueKey<T> extends LocalKey {
  /// Creates a key that delegates its [operator==] to the given value.
  const ValueKey(this.value);
  /// The value to which this key delegates its [operator==]
  final T value;
  @override
  bool operator ==(dynamic other) {
    if (other.runtimeType != runtimeType)
      return false;
    final ValueKey<T> typedOther = other;
    return value == typedOther.value;
  }
  @override
  int get hashCode => hashValues(runtimeType, value);
}

派生自LocalKey,接受一个泛型类,重写了==方法和hash方法,需要同时校验runtimeType和value

使用场景

如果您有一个 Todo List 应用程序,它将会记录你需要完成的事情。我们假设每个 Todo 事情都各不相同,而你想要对每个 Todo 进行滑动删除操作。
这时候就需要使用 **ValueKey。

  • PageStoreKey
class PageStorageKey<T> extends ValueKey<T> {
  /// Creates a [ValueKey] that defines where [PageStorage] values will be saved.
  const PageStorageKey(T value) : super(value);
}

PageStorageKey是继承自ValueKey,传入一个value,它是用于保存Scrollable的偏移
Scrollable(实际是ScrollPositions)使用PageStorage保存它们的滚动偏移,每次滚动完成时,存储的滚动信息都会更新。
PageStorage用于保存和恢复比widget生命周期更长的值,这些值存储在per-route Map中,它的key由widget及它的祖先的PageStorageKeys定义

使用场景

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

  • ObjectKey
class ObjectKey extends LocalKey {
  /// Creates a key that uses [identical] on [value] for its [operator==].
  const ObjectKey(this.value);
  /// The object whose identity is used by this key's [operator==].
  final Object value;
  @override
  bool operator ==(dynamic other) {
    if (other.runtimeType != runtimeType)
      return false;
    final ObjectKey typedOther = other;
    return identical(value, typedOther.value);
  }
  @override
  int get hashCode => hashValues(runtimeType, identityHashCode(value));
}

ObjectKey也是集成自LocalKey,而且构造方法也是需要传入一个value,但是这个value是Object类型的,也就是说可以传任意类型,identical 方法返回的是两个Object的hashCode是否相等,当runtimeType跟value.hashCode都相等的情况下,ObjectKey才会被认为相等,它跟ValueKey的区别在于它比较的是value的引用,而ValueKey是直接比较值

使用场景

如果你有一个生日应用,它可以记录某个人的生日,并用列表显示出来,同样的还是需要有一个滑动删除操作。
我们知道人名可能会重复,这时候你无法保证给 Key 的值每次都会不同。但是,当人名和生日组合起来的 Object 将具有唯一性。
这时候你需要使用 ObjectKey

  • UniqueKey
/// A key that is only equal to itself.
class UniqueKey extends LocalKey {
  /// Creates a key that is equal only to itself.
  // ignore: prefer_const_constructors_in_immutables , never use const for this class
  UniqueKey();
}

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

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