咱們平時必定接觸過不少的 Widget,好比 Container、Row、Column 等,它們在咱們繪製界面的過程當中發揮着重要的做用。可是不知道你有沒有注意到,在幾乎每一個 Widget 的構造函數中,都有一個共同的參數,它們一般在參數列表的第一個,那就是 Key。markdown
可是在咱們構造這些 Widget 的時候,又不多指定並傳入wfdh這個參數,那麼這個參數到底是幹嗎的呢?它又有什麼做用呢?app
咱們先來看看 Key 這個類吧,它是個虛擬類,有子類 LocalKey
和 GlobalKey
,它們也是虛擬類,又有各自的子類。less
它們有什麼區別呢?具體有什麼使用場景呢?帶着這些疑問,咱們繼續向下看。ide
咱們首先看一個 demo:有兩個色塊和一個按鈕,點擊按鈕後,兩個色塊會進行交換。函數
代碼部分,兩個無狀態的色塊 Widget,點擊按鈕後,交換 Widget 在 List 中的位置並進行刷新。oop
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 中,那麼狀況又如何呢?此時發現,不管咱們怎麼點擊交換按鈕,色塊的位置或者顏色都不會再交換了。可是若是將顏色的屬性存儲在 Widget 中而不是 State 中,那麼此時的交換效果又變得正常了。這是什麼緣由呢?佈局
咱們知道,Flutter 中 Widget 並非最終繪製在屏幕上的對象,它只是用來配置和儲存測繪數據的一種媒介以及構建真正的圖像和用戶交互的橋樑,而 Element 則是視圖樹的骨架,標識了各個控件在視圖樹上的一種樹形結構的關係,而真正被繪製到屏幕上的則是 RenderObject。動畫
那麼,第一個例子中的兩個色塊爲何能夠交換呢?ui
左邊是咱們佈局的視圖結構,framework 會構造出與之一一相對應的 Element 的結構關係。當咱們交換兩個色塊後,左邊的 WIdget 關係中,兩個 Widget 的樹型相對位置發生變化,此時經過 setState
方法通知 framework 視圖樹可能有變化須要刷新,Element 樹就會被遍從來和 Widget 樹一一進行對比是否還相等,包括類中的屬性和成員。當發現表示顏色的屬性再也不相對應相等時,就能夠知道視圖樹發生了改變,須要進行刷新,Element 樹會從新和 Widget 樹進行對應構造,屏幕上的兩個色塊交換的效果就會被繪製出來。this
簡而言之,界面之因此會刷新,就是由於 framework 發現了視圖樹產生變化,因此知道本身須要刷新進行從新繪製。其中重點就是 framework 可以發現視圖樹的變化。
那爲何咱們換用 StatefulWidget 並將顏色屬性存儲在 State 中後就沒法進行交換了呢?緣由偏偏就是在於 framework 此時已經沒法發現視圖樹產生變化了。
這種狀況下,Widget 和 Element 樹和以前的狀況大致相同,可是不一樣的是,如今每一個小色塊 Widget 都關聯一個 State,用來管理它們的狀態,而顏色屬性就儲存在 State 中。Element 樹有和 Widget 樹一一對應的 ELement 節點,可是 Element 並無用來管理狀態的 State。
在咱們點擊按鈕進行交換後,一樣的,Widget 的樹中的位置發生變化,而後 setState
方法通知 framework 可能有視圖樹的變化須要注意,Element 被指派去和 Widgewfdhwfdhwfdhwfdht 樹進行比對,可是因爲 StatefulColorBlock 中不存儲任何狀態值(或者說不存在任何可以區別的不相同的狀態值),因此 Element 遍歷後長抒一口氣:「沒有不同的地方,不須要進行樹的更新,繼續划水……」,本着偷懶的原則,視圖樹並無接收到須要刷新的指令,就不作任何處理工做,儘管實質上它們的 State 已經不一樣,可是這並不能引發刷新機制的注意,所以帶來的結果就是不管咱們怎麼點擊交換按鈕,界面都紋絲不動,不會進行任何更新。
至此,對於將顏色屬性存儲在 Widget 中而不是 State 中的交換效果正常的原理也就顯而易見了。
那麼,對於上面說的顏色沒法交換的狀況甚或其餘各類相似的狀況,咱們在開發中該怎麼處理呢?
就在此時,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 樹的時候,發現了沒法對應的 Key,因此斷定視圖樹的結構發生了變化,有必要進行刷新,因此會對兩個色塊進行重繪,因此咱們就能看到色塊交換的效果啦。
flutter 源碼在 Key 類上的註釋文檔開門見山地說明了,Key 是 Widget、Element 和 SemanticsNode 的「身份證」。幫助它們進行lwlwlwlwlw有效的區分。並且 Key 是視圖樹的更新策略的重要依據。下面摘錄 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 在視圖樹上的替換規則。若是兩個 Widget 的 runtimeType 和 Key 都是相等的(用 ==
操做符比較結果爲真),那麼新的 Widget 會經過更新其下的 Element(經過調用 Element.update
並傳入新的 Widget 的方式)。不然,舊的 Element 就會被從視圖樹上刪除,新的 Widget 掛載到新的 Element,新的 Element 再被插入視圖樹上。
另外,若是用 GlobalKey 做爲 Widget 的 Key,可以使該 Widget 在整個視圖樹上移動(其父節點會發生變動)而不丟失狀態。若是有一個新的 Widget,它和原先在此位置的 Widget 的 Key 或者類型不相同,可是在前一幀有一個和它相同的 GlobalKey 的 Widget,位置在其餘地方,那麼那個前一幀的 Widget 所依附的 Element 就會移動到新的 Widget 所在的視圖樹的位置。
對 Key 的工做原理有了一個大體的瞭解,那麼咱們再詳細看看 Key 的那些實現的子類們。
UniqueKey
只和它本身斷定相等的 Key,它不能被 const 修飾構造函數,由於若是被 const 關鍵字修飾,那麼它的全部實例都將是同一個,違背只能和它本身相等的原則。
ValueKey
能夠指定其值的 Key,其值被泛型約束。當且僅當兩個 ValueKey 能被 == 操做符斷定相等,它們纔是相等的。
PageStorageKey
它是 ValueKey 的子類,也能夠指定一個被泛型約束的值。正如其名,它和 PageStorage 類密切相關。PageStorage 是一個保存和恢復值的 Widget 子類。而 PageStorageKey 正是用來在 widget 重建以後找回和恢復存儲的值。在每一個路由中,有一個存儲數據的 Map,而該 Map 的 Key 正是由 PageStorageKey 所定義的值來決定的。因此,PageStorage 建立時所傳入的值不該該隨着 widget 的重建發生變化。
ObjectKey
ObjectKey 和 ValueKey 很相似,也是傳入一個值,並經過其值來比較兩者是否相等。可是它們有兩個不一樣點。
ObjectKey 的 value 不是泛型約束的,而是一個 Object 對象;
兩者的重載操做符方法 ==
內容不同。
// 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 的子類以前,咱們先來看看這個雖然是虛擬類但其實比起其實現的子類可能更重要的父類。下面摘錄 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 在從原來的位置被刪除到在新的位置「紮根」須要在一個動畫幀內完成。
GlobalKey 是高昂的,若是你不是真的須要它,儘可能考慮使用 Key、ValueKey、ObjectKey 或 UniqueKey 代替。
視圖樹上的兩個 widget 不可能同時擁有一樣的 GlobalKey,若是嘗試如此,會沒法經過 assert 語句的斷言。
咱們這裏須要注意三點:
咱們着重解釋3,由於2就是3的結果。
注意到 GlobalKey 中有一個靜態對象 static final Map<GlobalKey, Element> _registry = <GlobalKey, Element>{}
,它是 GlobalKey 和 Element 的鍵值對,而其數據的填充是在 _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()
中被調用。因此經過這個存儲下來的 element,GlobalKey 就能夠拿到全部與其相關的對象——BuildContext、Element、Widget 以及 State 等。
好了,聊了這麼多的 GlobalKey,下面看看它的兩個子類。
LabeledGlobalKey
在 GlobalKey 的基礎上增長了一個 _debugLabel
屬性,用來在 log 等輸出作調試用,並且 GlobalKey 的工廠構造函數返回的其實就是 LabeledGlobalKey。
GlobalObjectKey
聯想 ObjectKey 和 LocalKey 的關係,它能夠看作本身指定 value 的 GlobalKey,可是這就可能形成因指定了相同的 value 而致使的衝突問題,解決方法是構造其私有的子類。
class _MyKey extends GlobalObjectKey {
const _MyKey(Object value) : super(value);
}
複製代碼
通過上面對各類 Key 的介紹,其各自適用場景也很明顯了。由於 GlobalKey 代價高昂,因此除非在一些須要在不一樣視圖樹節點同步 widget、element 和 State 的屬性的狀況下,儘可能使用 LocalKey,而各個 LocalKey 的不一樣特性又決定了它們不一樣的使用場景。
Key 在整個 flutter 的結構體系中很小,咱們對他最陌生又最熟悉。搞懂它的做用,那咱們在下次點開 widget 的構造函數時,發現躺在參數列表第一個的 Key,咱們也能夠微微一笑:我認識你。
(以上部份內容來自Flutter 官方 YouTube 介紹視頻和 Flutter 源碼註釋文檔)