在 Flutter
中,大概你們都知道如何更新界面視圖: 經過修改 Stata
去觸發 Widget
重建,觸發和更新的操做是 Flutter
框架作的。 可是有時即便修改了 State
,Flutter
框架好像也沒有觸發 Widget
重建,
其中就隱含了 Flutter
框架內部的更新機制,在某些狀況下須要結合使用 Key
,才能觸發真正的「重建」。
下面將從 3 個方面 (When, Where, Which) 說明如何在合理的時間和地點使用合理的 Key。html
Key
需求: 點擊界面上一個按鈕,而後交換行中的兩個色塊。git
使用 StatelessWidget
(StatelessColorfulTile
) 作 child
(tiles
):github
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); }
使用 StatefulWidget
(StatefulColorfulTile
) 作 child
(tiles
):api
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(), ]; }
爲何使用 StatefulWidget
就不能成功更新呢? 須要先了解下面的內容。less
在 Flutter 框架中,視圖維持在樹的結構中,咱們編寫的 Widget 一個嵌套一個,最終組合爲一個 Tree。ide
在第一種使用 StatelessWidget
的實現中,當 Flutter 渲染這些 Widgets 時,Row
Widget 爲它的子 Widget 提供了一組有序的插槽。對於每個 Widget,Flutter 都會構建一個對應的 Element
。構建的這個 Element
Tree 至關簡單,僅保存有關每一個 Widget
類型的信息以及對子Widget
的引用。你能夠將這個 Element
Tree 當作就像你的 Flutter App 的骨架。它展現了 App 的結構,但其餘信息須要經過引用原始Widget
來查找。ui
當咱們交換行中的兩個色塊時,Flutter 遍歷 Widget
樹,看看骨架結構是否相同。它從 Row
Widget 開始,而後移動到它的子 Widget,Element 樹檢查 Widget 是否與舊 Widget 是相同類型和 Key
。 若是都相同的話,它會更新對新 widget 的引用。在咱們這裏,Widget 沒有設置 Key,因此Flutter
只是檢查類型。它對第二個孩子作一樣的事情。因此 Element 樹將根據 Widget 樹進行對應的更新。3d
當 Element Tree 更新完成後,Flutter 將根據 Element Tree 構建一個 Render Object Tree,最終開始渲染流程。
當使用 StatefulWidget
實現時,控件樹的結構也是相似的,只是如今 color 信息沒有存儲控件自身了,而是在外部的 State 對象中。
如今,咱們點擊按鈕,交換控件的次序,Flutter 將遍歷 Element 樹,檢查 Widget 樹中 Row
控件而且更新 Element 樹中的引用,而後第一個 Tile 控件檢查它對應的控件是不是相同類型,它發現對方是相同的類型; 而後第二個 Tile 控件作相同的事情,最終就致使 Flutter 認爲這兩個控件都沒有發生改變。Flutter 使用 Element 樹和它對應的控件的 State 去肯定要在設備上顯示的內容, 因此 Element 樹沒有改變,顯示的內容也就不會改變。
如今,爲 StatefulColorfulTile
傳遞一個 Key
對象:
void initState() { super.initState(); tiles = [ // 使用 UniqueKey StatefulColorfulTile(key: UniqueKey()), StatefulColorfulTile(key: UniqueKey()), ]; }
再次運行:
成功 swap!
添加了 Key
以後的結構:
當如今執行 swap 時, Element 數中 StatafulWidget 控件除了比較類型外,還會比較 key
是否相等:
只有類型和key
都匹配時,纔算找到對應的 Widget。因而在 Widget Tree 發生交換後,Element Tree 中子控件和原始控件對應關係就被打亂了,因此 Flutter 會重建 Element Tree,直到控件們正確對應上。
因此,如今 Element 樹正確更新了,最終就會顯示交換後的色塊。
若是要修改集合中的控件的順序或數量,Key
會頗有用。
正常狀況下應該在當前 Widget 樹的頂級 Widget 中設置。
回到 StatefulColorfulTile
例子中,爲每一個色塊添加一個 Padding
,同時 key
仍是設置在相同的地方:
@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()), ), ]; }
當點擊按鈕發生交換以後,能夠看到兩個色塊的顏色會隨機改變,可是個人預期是兩個固定的顏色彼此交換。
當Widget 樹中兩個 Padding
發生了交換,它們包裹的色塊也就發生了交換:
而後 Flutter 將進行檢查,以便對 Element 樹進行對應的更新: Flutter 的 Elemetn to Widget
匹配算法將一次只檢查樹的一個層級:
Padding
Widget 都正確匹配。Key
不匹配,就停用該 Tile Element,刪除 Widget 和 Element 之間的鏈接Key
是 UniqueKey
, 它是一個 LocalKey
LocalKey
的意思是: 當 Widget 與 Element 匹配時,Flutter 只在樹中特定級別內查找匹配的 Key。所以 Flutter 沒法在同級中找到具備該 Key 的 Tile Widget,因此它會建立一個新 Element 並初始化一個新 State。 就是這個緣由,形成色塊顏色發生隨機改變,每次交換至關於生成了兩個新的 Widget。
Key
設置到上層 Widget Padding
上當 Widget 樹中兩個 Padding
發生交換以後,Flutter 就能根據 Padding
上 Key
的變化,更新 Element
樹中的兩個 Padding
,從而實現交換。
@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(), ), ]; }
Key
的目的在於爲每一個 Widget 指明一個惟一的身份,使用何種 Key
就要依具體的使用場景決定。
例如在一個 ToDo
列表應用中,每一個 Todo
Item 的文本是恆定且惟一的。這種狀況,適合使用 ValueKey
,value 是文本。
假設,每一個子 Widget 都存儲了一個更復雜的數據組合,好比一個用戶信息的地址簿應用。任何單個字段(如名字或生日)可能與另外一個條目相同,但每一個數據組合是惟一的。在這種狀況下, ObjectKey
最合適。
若是集合中有多個具備相同值的 Widget,或者若是您想確保每一個 Widget 與其餘 Widget 不一樣,則可使用 UniqueKey
。 在咱們的例子中就使用了 UniqueKey
,由於咱們沒有將任何其餘常量數據存儲在咱們的色塊上,而且在構建 Widget 以前咱們不知道顏色是什麼。
不要在 Key
中使用隨機數,若是你那樣設置,那麼當每次構建 Widget 時,都會生成一個新的隨機數,Element 樹將不會和 Widget 樹作一致的更新。
Global Keys有兩種用途。
在第二種狀況下,您可能但願驗證密碼,但不但願與樹中的其餘 Widget 共享該狀態信息,可使用 GlobalKey<FromState>
持有一個表單 Form
的 State
。 Flutter.dev 上有這個例子Building a form with validation。
其實 GlobalKeys 看起來有點像全局變量。有也其餘更好的方法達到 GlobalKeys 的做用,好比 InheritedWidget、Redux 或 Block Pattern。
如何合理適當的使用 Key
:
Key
。例如: 當修改相同類型的 Widget 集合(如列表中)時Key
設置在要指明惟一身份的 Widget 樹的頂部Key
參考
- https://flutter.dev/docs/development/ui/widgets-intro#keys
- https://api.flutter.dev/flutter/foundation/Key-class.html
- https://www.youtube.com/watch?v=kn0EOS-ZiIc
- https://www.yuque.com/xytech/flutter/tge705
上文涉及的例子代碼: https://github.com/stefanJi/fullter-playgroud