理解 Flutter 中的 Key

概覽

Flutter 中,大概你們都知道如何更新界面視圖: 經過修改 Stata 去觸發 Widget 重建,觸發和更新的操做是 Flutter 框架作的。 可是有時即便修改了 StateFlutter 框架好像也沒有觸發 Widget 重建,
其中就隱含了 Flutter 框架內部的更新機制,在某些狀況下須要結合使用 Key,才能觸發真正的「重建」。
下面將從 3 個方面 (When, Where, Which) 說明如何在合理的時間和地點使用合理的 Key。html

When: 何時該使用 Key

實戰例子

需求: 點擊界面上一個按鈕,而後交換行中的兩個色塊。git

StatelessWidget 實現

使用 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);
}
結果

PositionedTiles

成功實現需求 ^_^

StatefulWidget 實現

使用 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);
}

修改外部容器 PositionedTilestiles:框架

@override
  void initState() {
    super.initState();
    tiles = [
      StatefulColorfulTile(),
      StatefulColorfulTile(),
    ];
  }
結果

PositionedTiles

貌似沒效果 -_-

爲何使用 StatefulWidget 就不能成功更新呢? 須要先了解下面的內容。less

Fluuter 對 Widget 的更新原理

在 Flutter 框架中,視圖維持在樹的結構中,咱們編寫的 Widget 一個嵌套一個,最終組合爲一個 Tree。ide

StatelessWidget

在第一種使用 StatelessWidget 的實現中,當 Flutter 渲染這些 Widgets 時,Row Widget 爲它的子 Widget 提供了一組有序的插槽。對於每個 Widget,Flutter 都會構建一個對應的 Element。構建的這個 Element Tree 至關簡單,僅保存有關每一個 Widget 類型的信息以及對子Widget 的引用。你能夠將這個 Element Tree 當作就像你的 Flutter App 的骨架。它展現了 App 的結構,但其餘信息須要經過引用原始Widget來查找。ui

StatelessWidget Tree & Element Tree

當咱們交換行中的兩個色塊時,Flutter 遍歷 Widget 樹,看看骨架結構是否相同。它從 Row Widget 開始,而後移動到它的子 Widget,Element 樹檢查 Widget 是否與舊 Widget 是相同類型和 Key。 若是都相同的話,它會更新對新 widget 的引用。在咱們這裏,Widget 沒有設置 Key,因此Flutter只是檢查類型。它對第二個孩子作一樣的事情。因此 Element 樹將根據 Widget 樹進行對應的更新。3d

swap以後

當 Element Tree 更新完成後,Flutter 將根據 Element Tree 構建一個 Render Object Tree,最終開始渲染流程。

相似這樣的渲染流程

StatefulWidget

當使用 StatefulWidget 實現時,控件樹的結構也是相似的,只是如今 color 信息沒有存儲控件自身了,而是在外部的 State 對象中。

StatefulWidget Tree & Element Tree

如今,咱們點擊按鈕,交換控件的次序,Flutter 將遍歷 Element 樹,檢查 Widget 樹中 Row 控件而且更新 Element 樹中的引用,而後第一個 Tile 控件檢查它對應的控件是不是相同類型,它發現對方是相同的類型; 而後第二個 Tile 控件作相同的事情,最終就致使 Flutter 認爲這兩個控件都沒有發生改變。Flutter 使用 Element 樹和它對應的控件的 State 去肯定要在設備上顯示的內容, 因此 Element 樹沒有改變,顯示的內容也就不會改變。

swap以後

StatefullWidget 結合 Key

如今,爲 StatefulColorfulTile 傳遞一個 Key 對象:

void initState() {
  super.initState();
  tiles = [
    // 使用 UniqueKey
    StatefulColorfulTile(key: UniqueKey()),
    StatefulColorfulTile(key: UniqueKey()),
  ];
}

再次運行:

PositionedTiles

成功 swap!

添加了 Key 以後的結構:

PositionedTiles

當如今執行 swap 時, Element 數中 StatafulWidget 控件除了比較類型外,還會比較 key 是否相等:

檢查比較

只有類型和key 都匹配時,纔算找到對應的 Widget。因而在 Widget Tree 發生交換後,Element Tree 中子控件和原始控件對應關係就被打亂了,因此 Flutter 會重建 Element Tree,直到控件們正確對應上。

重建

因此,如今 Element 樹正確更新了,最終就會顯示交換後的色塊。

交換完畢

使用場景

若是要修改集合中的控件的順序或數量,Key 會頗有用。

Where: 在哪設置 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 匹配算法將一次只檢查樹的一個層級:

檢查

  1. 在第一級,Padding Widget 都正確匹配。

檢查

  1. 在第二級,Flutter 注意到 Tile 控件的 Key 不匹配,就停用該 Tile Element,刪除 Widget 和 Element 之間的鏈接

檢查

  1. 咱們這裏使用的 KeyUniqueKey, 它是一個 LocalKey

LocalKey 的意思是: 當 Widget 與 Element 匹配時,Flutter 只在樹中特定級別內查找匹配的 Key。所以 Flutter 沒法在同級中找到具備該 Key 的 Tile Widget,因此它會建立一個新 Element 並初始化一個新 State。 就是這個緣由,形成色塊顏色發生隨機改變,每次交換至關於生成了兩個新的 Widget。

  1. 解決這個問題: 將 Key 設置到上層 Widget Padding

當 Widget 樹中兩個 Padding 發生交換以後,Flutter 就能根據 PaddingKey 的變化,更新 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(),
     ),
   ];
 }

根據 Padding Key

Which: 該使用哪一種類型的 Key

Key 的目的在於爲每一個 Widget 指明一個惟一的身份,使用何種 Key 就要依具體的使用場景決定。

  • ValueKey

例如在一個 ToDo 列表應用中,每一個 Todo Item 的文本是恆定且惟一的。這種狀況,適合使用 ValueKey,value 是文本。

  • ObjectKey

假設,每一個子 Widget 都存儲了一個更復雜的數據組合,好比一個用戶信息的地址簿應用。任何單個字段(如名字或生日)可能與另外一個條目相同,但每一個數據組合是惟一的。在這種狀況下, ObjectKey 最合適。

  • UniqueKey

若是集合中有多個具備相同值的 Widget,或者若是您想確保每一個 Widget 與其餘 Widget 不一樣,則可使用 UniqueKey。 在咱們的例子中就使用了 UniqueKey,由於咱們沒有將任何其餘常量數據存儲在咱們的色塊上,而且在構建 Widget 以前咱們不知道顏色是什麼。

不要在 Key 中使用隨機數,若是你那樣設置,那麼當每次構建 Widget 時,都會生成一個新的隨機數,Element 樹將不會和 Widget 樹作一致的更新。

  • GlobalKeys

Global Keys有兩種用途。

  • 它們容許 Widget 在應用中的任何位置更改父級而不會丟失 State ,或者可使用它們在 Widget 樹 的徹底不一樣的部分中訪問有關另外一個 Widget 的信息。
    • 好比: 要在兩個不一樣的屏幕上顯示相同的 Widget,同時保持相同的 State,則須要使用 GlobalKeys。

    複用 Widget

  • 在第二種狀況下,您可能但願驗證密碼,但不但願與樹中的其餘 Widget 共享該狀態信息,可使用 GlobalKey<FromState> 持有一個表單 FormState。 Flutter.dev 上有這個例子Building a form with validation

其實 GlobalKeys 看起來有點像全局變量。有也其餘更好的方法達到 GlobalKeys 的做用,好比 InheritedWidget、Redux 或 Block Pattern。

總結

如何合理適當的使用 Key:

  1. When: 當您想要保留 Widget 樹的狀態時,請使用 Key。例如: 當修改相同類型的 Widget 集合(如列表中)時
  2. Where: 將 Key 設置在要指明惟一身份的 Widget 樹的頂部
  3. Which: 根據在該 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

相關文章
相關標籤/搜索