iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 12
0
Mobile Development

Why Flutter why? 從表層到底層,從如何到為何。系列 第 12

days[11] = "為什麼要有key?"

我覺得key是一個蠻奇妙的東西。它是Widget(base class)——整個Flutter中最重要的class——唯一的一個參數,但它的應用場景卻實在不是那麼多,但所有官方的數百個Widget(child class)卻又都必須繼承它,以備不時之需。那麼我們到底什麼時候會用到key?它是怎麼運作的?為什麼它好像很重要但實際上又不是那麼重要?今天就讓我們來一探究竟。

當然,關於key的用途和運作方式,其實官方介紹影片已經講解得非常不錯了,非常推薦大家前往觀看。不過,即使是官方影片也有我不是很喜歡的部份,也就是影片一開始說的:

Keys preserve state when widgets move around in your widget tree.

這句話的內容當然完全沒錯,只是當妳在介紹key時劈頭就是這句話,感覺好像會讓人以為,保存狀態就是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.
/// .....
@immutable
abstract class Key {
}

這樣的邏輯是不是比較通順了?key基本上就是給widget, element的一個ID。有了ID之後可以做的其中一件事情,是判斷兩個widget是否相同,進而決定element是要更新還是重建。以上可能只是一個相當鑽牛角尖的小地方,但如果你有注意到的話,這整個系列就是一個打破沙鍋問到底的系列...

無論如何,官方的介紹影片還是非常不錯的,關於key如何影響widget tree的更新機制解釋得滿清楚的。我本來想進一步深入研究這部份的source code是怎麼運作的,但光是找到這個影響發生的地點就花了不少時間,而最後找到的source code真的極為複雜,讓我馬上決定之後再另外開一篇來解釋。有興趣自我挑戰的人可以偷看一下RenderObjectElement.updateChildren就會知道我在說什麼了。

總之,今天就先簡單介紹一下各種key的不同應用場景吧:
https://ithelp.ithome.com.tw/upload/images/20200912/20129053BsLeShm4fN.png
首先可以看到key分成兩大類:LocalKey和GlobalKey,其中LocalKey是什麼呢?

/// A key that is not a [GlobalKey].
/// ......
abstract class LocalKey extends Key {
  /// Default constructor, used by subclasses.
  const LocalKey() : super.empty();
}

哈哈...官方文件真幽默...沒辦法我們先來看看GlobalKey:

abstract class GlobalKey<T extends State<StatefulWidget>> extends Key {
  ....
  static final Map<GlobalKey, Element> _registry = <GlobalKey, Element>{};
  ....
  void _register(Element element) {
    ....
    _registry[this] = element;
  }
  ....
}

這裡就可以大概看出GlobalKey的運作方式了,有個static map從GlobalKey映射到Element,每次呼叫register就會把Globalkey自己映射到傳入的Element。再來看看誰在什麼時候呼叫了_register:

  // in Element
  void mount(Element parent, dynamic newSlot) {
    ....
    final Key key = widget.key;
    if (key is GlobalKey) {
      key._register(this);
    }
    ....
  }

原來是我們的老朋友Element.mount。這時候就能理解為什麼須要LocalKey這個type了,它唯一的功用就是為了在Element.mount時和GlobalKey做出區別,使它不會被永久存入static map裡。因為_register時不但儲存了key對應的Element,可能也連帶儲存了Element帶有的RenderObject。這也就是為什麼我們可能會在別的地方看到別人說GlobalKey很貴,總之若非必要請盡量使用LocalKey。

GlobalKey最主要的功用有兩個,第一是當我們想要把widget直接移動到不同的parent widget,同時保存它的state時。例如: https://ithelp.ithome.com.tw/upload/images/20200912/20129053zDrKRSMjYQ.png
如果我們沒加GlobalKey就會發現Switch的State被重建了。

第二是當我們想要在任何地方取得Widget、Element(BuildContext)或State的資訊時,例如:

void main() {
  final key = GlobalKey();
  runApp(Container(color: Colors.white, key: key));
  WidgetsBinding.instance.addPostFrameCallback((_) {
    print(key.currentWidget); // Container-[GlobalKey#f9f3d](bg: Color(0xffffffff))
  });
}

現在我們可以回頭來看看not a GlobalKey的LocalKey了,它主要的應用就是讓我們在移動StatefulWidget List時,能夠保持正確的State:
https://ithelp.ithome.com.tw/upload/images/20200912/20129053IZycX1qKUk.png
這裡用顏色來表示同一個object,當黃色switch被拉到第二個位置時,若沒有設定key,就會使Widget.canUpdate回傳true,而導致Element和其背後的State沒有正確的跟著移動。我們之後會再看到這部份的詳細程式碼。

接著繼續來看看LocalKey的三種類型,首先是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 ==(Object other) {
    if (other.runtimeType != runtimeType)
      return false;
    return other is ValueKey<T>
        && other.value == value;
  }
  ....
}

key最重要的工作就是比較兩個key是否相同,因此大多都會複寫==運算子來實作key的等同比對。ValueKey很單純的就是透過自身帶的一個value來做比較。ValueKey通常會用在我們的Model含有本身含有id時,可以做成items.map((it) => Text(it.text, key: it.id))

接著是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 ==(Object other) {
    if (other.runtimeType != runtimeType)
      return false;
    return other is ObjectKey
        && identical(other.value, value);
  }
  .....

這裡要注意的是ObjectKey是用identical函數來判斷等同的,也就是說兩個key的value必須是同一個物件的參照,若只是物件內的屬性相同是不行的,例如:

class Foo {
  Foo(this.value);

  final String value;
}
void main() {
  final foo1 = Foo("Bar");
  final foo2 = Foo("Bar");
  print(foo1.value == foo2.value); // true
  print(ObjectKey(foo1) == ObjectKey(foo2)); // false
  print(ObjectKey(foo1) == ObjectKey(foo1)); // true
}

ObjectKey通常用在Model本身沒有id時,可以直接拿整個Model object來當key:items.map((it) => Text(it.text, key: it))。但這時就要注意items不能被重建,否則就會變成不同的object了。
另外還有一件事,這部份我一開始在看官方影片的時候也以為它會使用model裡的值來比較,而且我感覺講者自己也以為是這樣?因為影片裡講到類似"when the combination of fields is unique",但其實這跟field完全沒關係,只是單純比較object reference而已。但也有可能只是我對影片的解讀錯了。

總之,接下來是UniqueKey:

class UniqueKey extends LocalKey {
  /// Creates a key that is equal only to itself.
  ///
  /// The key cannot be created with a const constructor because that implies
  /// that all instantiated keys would be the same instance and therefore not
  /// be unique.
  // ignore: prefer_const_constructors_in_immutables , never use const for this class
  UniqueKey();

  @override
  String toString() => '[#${shortHash(this)}]';
}

基本上就是用在list item背後沒有model的時候,例如我們可以寫死一堆switch:

  Column(children: [
    Switch(key: UniqueKey(), ....),
    Switch(key: UniqueKey(), ....),
    Switch(key: UniqueKey(), ....),
    Switch(key: UniqueKey(), ....),
  ])

老實說這可能是LocalKey最有用的情境,因為在ValueKey和ObjectKey的情境,如果背後有個models的話,通常應該是由models來管理所有state,然後每次models更新時,重新build出List<StatelessWidget>才對,也就是widgets = f(models)的陳述式UI,根本不須要使用到List<StatefulWidget>和key的更新機制。


以上就是Flutter提供的各種Key和它們的使用情境,下次我們就來看看實際讓這些key發揮作用的恐怖程式碼吧,敬請期待!


上一篇
days[10] = "Plugin是怎麼運作的?"
下一篇
days[12] = "key是如何影響updateChildren的?"
系列文
Why Flutter why? 從表層到底層,從如何到為何。30

尚未有邦友留言

立即登入留言