我覺得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的不同應用場景吧:
首先可以看到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時。例如:
如果我們沒加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:
這裡用顏色來表示同一個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發揮作用的恐怖程式碼吧,敬請期待!