一锁蠕、Key
我們平時一定接觸過很多的 Widget惑艇,比如 Container蒿辙、Row、Column 等滨巴,它們在我們繪制界面的過程中發(fā)揮著重要的作用思灌。但是不知道你有沒有注意到,在幾乎每個 Widget 的構造函數(shù)中恭取,都有一個共同的參數(shù)泰偿,它們通常在參數(shù)列表的第一個,那就是 Key秽荤。
但是在我們構造這些 Widget 的時候甜奄,又很少指定并傳入wfdh這個參數(shù),那么這個參數(shù)究竟是干嘛的呢窃款?它又有什么作用呢课兄?
我們先來看看 Key 這個類吧,它是個虛擬類晨继,有子類 LocalKey
和 GlobalKey
烟阐,它們也是虛擬類,又有各自的子類紊扬。
它們有什么區(qū)別呢蜒茄?具體有什么使用場景呢?帶著這些疑問餐屎,我們繼續(xù)向下看檀葛。
二、煮個栗子
我們首先看一個 demo:有兩個色塊和一個按鈕腹缩,點擊按鈕后屿聋,兩個色塊會進行交換。
代碼部分藏鹊,兩個無狀態(tài)的色塊 Widget润讥,點擊按鈕后,交換 Widget 在 List 中的位置并進行刷新盘寡。
final List<Widget> _statelessTiles = [
StatelessColorBlock(),
StatelessColorBlock(),
];
_swapTiles() {
setState(() {
_statelessTiles.insert(1, _statelessTiles.removeAt(0));
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Row(
children: _statelessTiles,
),
floatingActionButton: FloatingActionButton(
onPressed: _swapTiles,
child: Icon(Icons.swap_horizontal_circle),
),
);
}
代碼運行效果良好楚殿,且符合我們的預期。但是竿痰,如果我們把色塊 Widget 由 StatelessWidget 變更為 StatefulWidget脆粥,并把顏色屬性存儲在 State 中,那么情況又如何呢菇曲?此時發(fā)現(xiàn)冠绢,無論我們怎么點擊交換按鈕,色塊的位置或者顏色都不會再交換了常潮。但是如果將顏色的屬性存儲在 Widget 中而不是 State 中弟胀,那么此時的交換效果又變得正常了。這是什么原因呢喊式?
我們知道孵户,F(xiàn)lutter 中 Widget 并不是最終繪制在屏幕上的對象,它只是用來配置和儲存測繪數(shù)據(jù)的一種媒介以及構建真正的圖像和用戶交互的橋梁岔留,而 Element 則是視圖樹的骨架夏哭,標識了各個控件在視圖樹上的一種樹形結構的關系,而真正被繪制到屏幕上的則是 RenderObject献联。
那么竖配,第一個例子中的兩個色塊為什么可以交換呢何址?
左邊是我們布局的視圖結構,framework 會構造出與之一一相對應的 Element 的結構關系进胯。當我們交換兩個色塊后用爪,左邊的 WIdget 關系中,兩個 Widget 的樹型相對位置發(fā)生變化胁镐,此時通過 setState
方法通知 framework 視圖樹可能有變化需要刷新偎血,Element 樹就會被遍歷來和 Widget 樹一一進行對比是否還相等,包括類中的屬性和成員盯漂。當發(fā)現(xiàn)表示顏色的屬性不再相對應相等時颇玷,就可以知道視圖樹發(fā)生了改變,需要進行刷新就缆,Element 樹會重新和 Widget 樹進行對應構造帖渠,屏幕上的兩個色塊交換的效果就會被繪制出來。
簡而言之竭宰,界面之所以會刷新阿弃,就是因為 framework 發(fā)現(xiàn)了視圖樹產(chǎn)生變化,所以知道自己需要刷新進行重新繪制羞延。其中重點就是 framework 能夠發(fā)現(xiàn)視圖樹的變化渣淳。
那為什么我們換用 StatefulWidget 并將顏色屬性存儲在 State 中后就無法進行交換了呢?原因恰恰就是在于 framework 此時已經(jīng)無法發(fā)現(xiàn)視圖樹產(chǎn)生變化了伴箩。
這種情況下入愧,Widget 和 Element 樹和之前的情況大體相同,但是不同的是嗤谚,現(xiàn)在每個小色塊 Widget 都關聯(lián)一個 State棺蛛,用來管理它們的狀態(tài),而顏色屬性就儲存在 State 中巩步。Element 樹有和 Widget 樹一一對應的 Element 節(jié)點旁赊,但是 Element 并沒有用來管理狀態(tài)的 State。
在我們點擊按鈕進行交換后椅野,同樣的终畅,Widget 的樹中的位置發(fā)生變化,然后 setState
方法通知 framework 可能有視圖樹的變化需要注意竟闪,Element 被指派去和 Widgewfdhwfdhwfdhwfdht 樹進行比對离福,但是由于 StatefulColorBlock 中不存儲任何狀態(tài)值(或者說不存在任何能夠區(qū)別的不相同的狀態(tài)值),所以 Element 遍歷后長抒一口氣:“沒有不一樣的地方炼蛤,不需要進行樹的更新妖爷,繼續(xù)劃水……”,本著偷懶的原則理朋,視圖樹并沒有接收到需要刷新的指令絮识,就不做任何處理工作绿聘,盡管實質(zhì)上它們的 State 已經(jīng)不同,但是這并不能引起刷新機制的注意次舌,因此帶來的結果就是無論我們怎么點擊交換按鈕斜友,界面都紋絲不動,不會進行任何更新垃它。
至此,對于將顏色屬性存儲在 Widget 中而不是 State 中的交換效果正常的原理也就顯而易見了烹看。
那么国拇,對于上面說的顏色無法交換的情況甚或其他各種類似的情況,我們在開發(fā)中該怎么處理呢惯殊?
就在此時酱吝,Key 作為一個 Key,它閃亮登場了土思。
我們稍微修改一下我們上面不生效的代碼务热。
final List<Widget> _statelessTiles = [
StatefulColorBlock(UniqueKey()),
StatefulColorBlock(UniqueKey()),
];
_swapTiles() {
setState(() {
_statefulTiles.insert(1, _statefulTiles.rwfdhemoveAt(0));
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Row(
children: _statefulTiles,
),
floatingActionButton: FloatingActionButton(
onPressed: _swapTiles,
child: Icon(Icons.swap_horizontal_circle),
),
);
}
再次點擊按鈕,噔噔己儒,兩個色塊又可以交換啦崎岂。
我們試著用上面分析出的結論舉一反三。因為每個 Widget 被指定了生成的不同的 UniqueKey闪湾,所以 Element 樹在比對 Widget 樹的時候冲甘,發(fā)現(xiàn)了無法對應的 Key,所以判定視圖樹的結構發(fā)生了變化途样,有必要進行刷新江醇,所以會對兩個色塊進行重繪,所以我們就能看到色塊交換的效果啦何暇。
三陶夜、品,你細品
flutter 源碼在 Key 類上的注釋文檔開門見山地說明了裆站,Key 是 Widget条辟、Element 和 SemanticsNode 的“身份證”。幫助它們進行l(wèi)wlwlwlwlw有效的區(qū)分宏胯。而且 Key 是視圖樹的更新策略的重要依據(jù)捂贿。下面摘錄 Widget 類中對成員 key 的注解。
Controls how one widget replaces another widget in the tree.
If the [runtimeType] and [key] properties of the two widgets are [operator==], respectively, then the new widget replaces the old widget by updating the underlying element (i.e., by calling [Element.update] with the new widget). Otherwise, the old element is removed from the tree, the new widget is inflated into an element, and the new element is inserted into the tree.
In addition, using a [GlobalKey] as the widget's [key] allows the element to be moved around the tree (changing parent) without losing state. When a new widget is found (its key and type do not match a previous widget in the same location), but there was a widget with that same global key elsewhere in the tree in the previous frame, then that widget's element is moved to the new location.
Generally, a widget that is the only child of another widget does not need an explicit key.
大概翻譯一下胳嘲。
Key 控制著 Widget 在視圖樹上的替換規(guī)則厂僧。如果兩個 Widget 的 runtimeType 和 Key 都是相等的(用 ==
操作符比較結果為真),那么新的 Widget 會通過更新其下的 Element(通過調(diào)用 Element.update
并傳入新的 Widget 的方式)了牛。否則颜屠,舊的 Element 就會被從視圖樹上刪除辰妙,新的 Widget 掛載到新的 Element,新的 Element 再被插入視圖樹上甫窟。
另外密浑,如果用 GlobalKey 作為 Widget 的 Key,能夠使該 Widget 在整個視圖樹上移動(其父節(jié)點會發(fā)生變更)而不丟失狀態(tài)粗井。如果有一個新的 Widget尔破,它和原先在此位置的 Widget 的 Key 或者類型不相同,但是在前一幀有一個和它相同的 GlobalKey 的 Widget浇衬,位置在其他地方懒构,那么那個前一幀的 Widget 所依附的 Element 就會移動到新的 Widget 所在的視圖樹的位置。
對 Key 的工作原理有了一個大致的了解耘擂,那么我們再詳細看看 Key 的那些實現(xiàn)的子類們胆剧。
-
UniqueKey
只和它自己判定相等的 Key,它不能被 const 修飾構造函數(shù)醉冤,因為如果被 const 關鍵字修飾秩霍,那么它的所有實例都將是同一個,違背只能和它自己相等的原則蚁阳。
-
ValueKey
可以指定其值的 Key铃绒,其值被泛型約束。當且僅當兩個 ValueKey 能被 == 操作符判定相等螺捐,它們才是相等的匿垄。
-
PageStorageKey
它是 ValueKey 的子類,也可以指定一個被泛型約束的值归粉。正如其名椿疗,它和 PageStorage 類密切相關。PageStorage 是一個保存和恢復值的 Widget 子類糠悼。而 PageStorageKey 正是用來在 widget 重建之后找回和恢復存儲的值届榄。在每個路由中,有一個存儲數(shù)據(jù)的 Map倔喂,而該 Map 的 Key 正是由 PageStorageKey 所定義的值來決定的铝条。所以,PageStorage 創(chuàng)建時所傳入的值不應該隨著 widget 的重建發(fā)生變化席噩。
-
ObjectKey
ObjectKey 和 ValueKey 很類似班缰,也是傳入一個值,并通過其值來比較二者是否相等悼枢。但是它們有兩個不同點埠忘。
ObjectKey 的 value 不是泛型約束的,而是一個 Object 對象;
-
二者的重載操作符方法
==
內(nèi)容不一樣莹妒。// ValueKey @override bool operator ==(Object other) { if (other.runtimeType != runtimeType) return false; return other is ValueKey<T> && other.value == value; } // ObjectKey @override bool operator ==(Object other) { if (other.runtimeType != runtimeType) return false; return other is ObjectKey && identical(other.value, value); }
==
是比較兩個對象是否相等名船,包括各個屬性及其值都相等,而identical()
是比較兩個引用是否指向同一個對象旨怠。
上面是 LocalKey 的子類們渠驼,下面我們再看看 GlobalKey 的子類們。在嘗試了解 GlobalKey 的子類之前鉴腻,我們先來看看這個雖然是虛擬類但其實比起其實現(xiàn)的子類可能更重要的父類迷扇。下面摘錄 GlobalKey 的注釋文檔。
A key that is unique across the entire app.
Global keys uniquely identify elements. Global keys provide access to other objects that are associated with those elements, such as [BuildContext]. For [StatefulWidget]s, global keys also provide access to [State].
Widgets that have global keys reparent their subtrees when they are moved from one location in the tree to another location in the tree. In order to reparent its subtree, a widget must arrive at its new location in the tree in the same animation frame in which it was removed from its old location in the tree.
Global keys are relatively expensive. If you don't need any of the features listed above, consider using a [Key], [ValueKey], [ObjectKey], or [UniqueKey] instead.
You cannot simultaneously include two widgets in the tree with the same global key. Attempting to do so will assert at runtime.
GlobalKey 在整個 app 都是唯一的爽哎。
GlobalKey 唯一地標識一個 element蜓席,它提供了通往與 element 相關類的通道,比如 BuildContext 類倦青,對于 StatefulWidget,比如 State盹舞。
擁有 GlobalKey 的對象在從視圖樹上的一個位置移動到另一個位置時會重新“認祖”产镐,而為了其子樹的重新構建,擁有 GlobalKey 的 element 在從原來的位置被刪除到在新的位置“扎根”需要在一個動畫幀內(nèi)完成踢步。
GlobalKey 是高昂的癣亚,如果你不是真的需要它,盡量考慮使用 Key获印、ValueKey述雾、ObjectKey 或 UniqueKey 代替。
視圖樹上的兩個 widget 不可能同時擁有同樣的 GlobalKey兼丰,如果嘗試如此玻孟,會無法通過 assert 語句的斷言。
我們這里需要注意三點:
GlobalKey 在整個 app 都是唯一的鳍征;
指定 GlobalKey 的對象黍翎,能夠在整個視圖樹上的任意節(jié)點間移動(而不丟失狀態(tài));
GlobalKey 能夠獲取 element 的相關類艳丛。
我們著重解釋3匣掸,因為2就是3的結果。
注意到 GlobalKey 中有一個靜態(tài)對象 static final Map<GlobalKey, Element> _registry = <GlobalKey, Element>{}
氮双,它是 GlobalKey 和 Element 的鍵值對碰酝,而其數(shù)據(jù)的填充是在 _register()
中。
void _register(Element element) {
assert(() {
if (_registry.containsKey(this)) {
assert(element.widget != null);
assert(_registry[this].widget != null);
assert(element.widget.runtimeType != _registry[this].widget.runtimeType);
_debugIllFatedElements.add(_registry[this]);
}
return true;
}());
_registry[this] = element;
}
而 _register()
方法在 Element 的 mount()
中被調(diào)用戴差。所以通過這個存儲下來的 element送爸,GlobalKey 就可以拿到所有與其相關的對象——BuildContext、Element、Widget 以及 State 等碱璃。
好了弄痹,聊了這么多的 GlobalKey,下面看看它的兩個子類嵌器。
-
LabeledGlobalKey
在 GlobalKey 的基礎上增加了一個
_debugLabel
屬性肛真,用來在 log 等輸出做調(diào)試用,而且 GlobalKey 的工廠構造函數(shù)返回的其實就是 LabeledGlobalKey爽航。 -
GlobalObjectKey
聯(lián)想 ObjectKey 和 LocalKey 的關系蚓让,它可以看做自己指定 value 的 GlobalKey,但是這就可能造成因指定了相同的 value 而導致的沖突問題讥珍,解決方法是構造其私有的子類历极。
class _MyKey extends GlobalObjectKey { const _MyKey(Object value) : super(value); }
四、適合的才是最好的
經(jīng)過上面對各種 Key 的介紹衷佃,其各自適用場景也很明顯了趟卸。因為 GlobalKey 代價高昂,所以除非在一些需要在不同視圖樹節(jié)點同步 widget氏义、element 和 State 的屬性的情況下锄列,盡量使用 LocalKey,而各個 LocalKey 的不同特性又決定了它們不同的使用場景惯悠。
Key 在整個 flutter 的結構體系中很小邻邮,我們對他最陌生又最熟悉。搞懂它的作用克婶,那我們在下次點開 widget 的構造函數(shù)時筒严,發(fā)現(xiàn)躺在參數(shù)列表第一個的 Key,我們也可以微微一笑:我認識你情萤。
(以上部分內(nèi)容來自Flutter 官方 YouTube 介紹視頻和 Flutter 源碼注釋文檔)