Flutter中的Key(二)

龍湫發表於2020-03-04

本文繼續分析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,失去了一致性

相關文章