通過實際案列理解 Flutter 中 Key 在其渲染機制中起到的作用柴梆,從而達到能在合理的時間和地點使用合理的 Key.
概覽
在 Flutter
中绍在,大概大家都知道如何更新界面視圖: 通過修改 State
去觸發(fā) Widget
重建雹有,觸發(fā)和更新的操作是 Flutter
框架做的霸奕。 但是有時即使修改了 State
,Flutter
框架好像也沒有觸發(fā) Widget
重建适揉,
其中就隱含了 Flutter
框架內(nèi)部的更新機制煤惩,在某些情況下需要結(jié)合使用 Key
魄揉,才能觸發(fā)真正的“重建”。
下面將從 3 個方面 (When, Where, Which) 說明如何在合理的時間和地點使用合理的 Key瓣俯。
When: 什么時候該使用 Key
實戰(zhàn)例子
需求: 點擊界面上一個按鈕彩匕,然后交換行中的兩個色塊摇零。
StatelessWidget 實現(xiàn)
使用 StatelessWidget
(StatelessColorfulTile
) 做 child
(tiles
):
class PositionedTiles extends StatefulWidget {
@override
State<StatefulWidget> createState() => PositionedTilesState();
}
class PositionedTilesState extends State<PositionedTiles> {
List<Widget> tiles;
@override
void initState() {
super.initState();
tiles = [
StatelessColorfulTile(),
StatelessColorfulTile(),
];
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: tiles))),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.sentiment_very_satisfied), onPressed: swapTiles));
}
當點擊按鈕時驻仅,更新 PositionedTilesState
中儲存的 tiles
:
void swapTiles() {
setState(() {
tiles.insert(1, tiles.removeAt(0));
});
}
}
class StatelessColorfulTile extends StatelessWidget {
final Color color = UniqueColorGenaretor.getColor();
StatelessColorfulTile({Key key}) : super(key: key);
@override
Widget build(BuildContext context) => buildColorfulTile(color);
}
結(jié)果
成功實現(xiàn)需求 _
StatefulWidget 實現(xiàn)
使用 StatefulWidget
(StatefulColorfulTile
) 做 child
(tiles
):
class StatefulColorfulTile extends StatefulWidget {
StatefulColorfulTile({Key key}) : super(key: key);
@override
State<StatefulWidget> createState() => StatefulColorfulTileState();
}
class StatefulColorfulTileState extends State<StatefulColorfulTile> {
// 將 Color 儲存在 StatefulColorfulTile 的 State StatefulColorfulTileState 中.
Color color;
@override
void initState() {
super.initState();
color = UniqueColorGenaretor.getColor();
}
@override
Widget build(BuildContext context) => buildColorfulTile(color);
}
修改外部容器 PositionedTiles
中 tiles
:
@override
void initState() {
super.initState();
tiles = [
StatefulColorfulTile(),
StatefulColorfulTile(),
];
}
結(jié)果
貌似沒效果 -_-
為什么使用 StatefulWidget
就不能成功更新呢毡泻? 需要先了解下面的內(nèi)容粘优。
Fluuter 對 Widget 的更新原理
在 Flutter 框架中,視圖維持在樹的結(jié)構(gòu)中丹墨,我們編寫的 Widget 一個嵌套一個贩挣,最終組合為一個 Tree。
StatelessWidget
在第一種使用 StatelessWidget
的實現(xiàn)中卵迂,當 Flutter 渲染這些 Widgets 時见咒,Row
Widget 為它的子 Widget 提供了一組有序的插槽挂疆。對于每一個 Widget,F(xiàn)lutter 都會構(gòu)建一個對應(yīng)的 Element
恃疯。構(gòu)建的這個 Element
Tree 相當簡單,僅保存有關(guān)每個 Widget
類型的信息以及對子Widget
的引用郑口。你可以將這個 Element
Tree 當做就像你的 Flutter App 的骨架犬性。它展示了 App 的結(jié)構(gòu),但其他信息需要通過引用原始Widget
來查找套利。
當我們交換行中的兩個色塊時肉迫,F(xiàn)lutter 遍歷 Widget
樹稿黄,看看骨架結(jié)構(gòu)是否相同杆怕。它從 Row
Widget 開始壳贪,然后移動到它的子 Widget违施,Element 樹檢查 Widget 是否與舊 Widget 是相同類型和 Key
瑟幕。 如果都相同的話收苏,它會更新對新 widget 的引用。在我們這里排吴,Widget 沒有設(shè)置 Key懦鼠,所以Flutter
只是檢查類型肛冶。它對第二個孩子做同樣的事情。所以 Element 樹將根據(jù) Widget 樹進行對應(yīng)的更新珊肃。
當 Element Tree 更新完成后伦乔,F(xiàn)lutter 將根據(jù) Element Tree 構(gòu)建一個 Render Object Tree董习,最終開始渲染流程皿淋。
StatefulWidget
當使用 StatefulWidget
實現(xiàn)時窝趣,控件樹的結(jié)構(gòu)也是類似的,只是現(xiàn)在 color 信息沒有存儲控件自身了缰儿,而是在外部的 State 對象中散址。
現(xiàn)在,我們點擊按鈕儒将,交換控件的次序对蒲,F(xiàn)lutter 將遍歷 Element 樹蹈矮,檢查 Widget 樹中 Row
控件并且更新 Element 樹中的引用,然后第一個 Tile 控件檢查它對應(yīng)的控件是否是相同類型蝠咆,它發(fā)現(xiàn)對方是相同的類型; 然后第二個 Tile 控件做相同的事情北滥,最終就導致 Flutter 認為這兩個控件都沒有發(fā)生改變再芋。Flutter 使用 Element 樹和它對應(yīng)的控件的 State 去確定要在設(shè)備上顯示的內(nèi)容, 所以 Element 樹沒有改變,顯示的內(nèi)容也就不會改變鉴逞。
StatefullWidget 結(jié)合 Key
現(xiàn)在华蜒,為 StatefulColorfulTile
傳遞一個 Key
對象:
void initState() {
super.initState();
tiles = [
// 使用 UniqueKey
StatefulColorfulTile(key: UniqueKey()),
StatefulColorfulTile(key: UniqueKey()),
];
}
再次運行:
成功 swap!
添加了 Key
之后的結(jié)構(gòu):
當現(xiàn)在執(zhí)行 swap 時, Element 數(shù)中 StatafulWidget 控件除了比較類型外贺拣,還會比較 key
是否相等:
只有類型和key
都匹配時,才算找到對應(yīng)的 Widget闪幽。于是在 Widget Tree 發(fā)生交換后涡匀,Element Tree 中子控件和原始控件對應(yīng)關(guān)系就被打亂了陨瘩,所以 Flutter 會重建 Element Tree级乍,直到控件們正確對應(yīng)上玫荣。
所以捅厂,現(xiàn)在 Element 樹正確更新了资柔,最終就會顯示交換后的色塊。
使用場景
如果要修改集合中的控件的順序或數(shù)量,Key
會很有用官边。
Where: 在哪設(shè)置 Key
正常情況下應(yīng)該在當前 Widget 樹的頂級 Widget 中設(shè)置注簿。
回到 StatefulColorfulTile
例子中,為每個色塊添加一個 Padding
捐晶,同時 key
還是設(shè)置在相同的地方:
@override
void initState() {
super.initState();
tiles = [
Padding(
padding: const EdgeInsets.all(8.0),
child: StatefulColorfulTile(key: UniqueKey()),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: StatefulColorfulTile(key: UniqueKey()),
),
];
}
當點擊按鈕發(fā)生交換之后惑灵,可以看到兩個色塊的顏色會隨機改變眼耀,但是我的預期是兩個固定的顏色彼此交換哮伟。
為什么產(chǎn)生問題
當Widget 樹中兩個 Padding
發(fā)生了交換,它們包裹的色塊也就發(fā)生了交換:
然后 Flutter 將進行檢查,以便對 Element 樹進行對應(yīng)的更新: Flutter 的 Elemetn to Widget
匹配算法將一次只檢查樹的一個層級:
- 在第一級肿仑,
Padding
Widget 都正確匹配。
- 在第二級勾邦,F(xiàn)lutter 注意到 Tile 控件的
Key
不匹配眷篇,就停用該 Tile Element荔泳,刪除 Widget 和 Element 之間的連接
- 我們這里使用的
Key
是UniqueKey
玛歌, 它是一個LocalKey
LocalKey
的意思是: 當 Widget 與 Element 匹配時,F(xiàn)lutter 只在樹中特定級別內(nèi)查找匹配的 Key创肥。因此 Flutter 無法在同級中找到具有該 Key 的 Tile Widget值朋,所以它會創(chuàng)建一個新 Element 并初始化一個新 State昨登。 就是這個原因,造成色塊顏色發(fā)生隨機改變撒强,每次交換相當于生成了兩個新的 Widget笙什。
- 解決這個問題: 將
Key
設(shè)置到上層 WidgetPadding
上
當 Widget 樹中兩個 Padding
發(fā)生交換之后琐凭,F(xiàn)lutter 就能根據(jù) Padding
上 Key
的變化,更新 Element
樹中的兩個 Padding
摆马,從而實現(xiàn)交換鸿吆。
@override
void initState() {
super.initState();
tiles = [
Padding(
key: UniqueKey(),
padding: const EdgeInsets.all(8.0),
child: StatefulColorfulTile(),
),
Padding(
key: UniqueKey(),
padding: const EdgeInsets.all(8.0),
child: StatefulColorfulTile(),
),
];
}
Which: 該使用哪種類型的 Key
Key
的目的在于為每個 Widget 指明一個唯一的身份,使用何種 Key
就要依具體的使用場景決定。
- ValueKey
例如在一個 ToDo
列表應(yīng)用中代虾,每個 Todo
Item 的文本是恒定且唯一的棉磨。這種情況学辱,適合使用 ValueKey
,value 是文本衙傀。
- ObjectKey
假設(shè)统抬,每個子 Widget 都存儲了一個更復雜的數(shù)據(jù)組合危队,比如一個用戶信息的地址簿應(yīng)用茫陆。任何單個字段(如名字或生日)可能與另一個條目相同,但每個數(shù)據(jù)組合是唯一的钱骂。在這種情況下挪鹏, ObjectKey
最合適讨盒。
- UniqueKey
如果集合中有多個具有相同值的 Widget,或者如果您想確保每個 Widget 與其他 Widget 不同禀苦,則可以使用 UniqueKey
遂鹊。 在我們的例子中就使用了 UniqueKey
秉扑,因為我們沒有將任何其他常量數(shù)據(jù)存儲在我們的色塊上,并且在構(gòu)建 Widget 之前我們不知道顏色是什么误澳。
不要在 Key
中使用隨機數(shù),如果你那樣設(shè)置裆装,那么當每次構(gòu)建 Widget 時哨免,都會生成一個新的隨機數(shù)毡琉,Element 樹將不會和 Widget 樹做一致的更新桅滋。
- GlobalKeys
Global Keys有兩種用途。
-
它們允許 Widget 在應(yīng)用中的任何位置更改父級而不會丟失 State 芍碧,或者可以使用它們在 Widget 樹 的完全不同的部分中訪問有關(guān)另一個 Widget 的信息号俐。
- 比如: 要在兩個不同的屏幕上顯示相同的 Widget吏饿,同時保持相同的 State,則需要使用 GlobalKeys贞远。
在第二種情況下蓝仲,您可能希望驗證密碼官疲,但不希望與樹中的其他 Widget 共享該狀態(tài)信息途凫,可以使用
GlobalKey<FromState>
持有一個表單Form
的State
。 Flutter.dev 上有這個例子Building a form with validation棚饵。
其實 GlobalKeys 看起來有點像全局變量噪漾。有也其他更好的方法達到 GlobalKeys 的作用且蓬,比如 InheritedWidget恶阴、Redux 或 Block Pattern。
總結(jié)
如何合理適當?shù)氖褂?Key
:
- When: 當您想要保留 Widget 樹的狀態(tài)時焦匈,請使用
Key
缓熟。例如: 當修改相同類型的 Widget 集合(如列表中)時 - Where: 將
Key
設(shè)置在要指明唯一身份的 Widget 樹的頂部 - Which: 根據(jù)在該 Widget 中存儲的數(shù)據(jù)類型選擇使用的不同類型的
Key
參考