理解 Flutter 中的 Key

通過實際案列理解 Flutter 中 Key 在其渲染機制中起到的作用柴梆,從而達到能在合理的時間和地點使用合理的 Key.

概覽

Flutter 中绍在,大概大家都知道如何更新界面視圖: 通過修改 State 去觸發(fā) Widget 重建雹有,觸發(fā)和更新的操作是 Flutter 框架做的霸奕。 但是有時即使修改了 StateFlutter 框架好像也沒有觸發(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é)果
PositionedTiles

成功實現(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);
}

修改外部容器 PositionedTilestiles:

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

貌似沒效果 -_-

為什么使用 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來查找套利。

StatelessWidget Tree & Element Tree"

當我們交換行中的兩個色塊時肉迫,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)的更新珊肃。

swap之后

當 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 對象中散址。

StatefulWidget Tree & Element Tree

現(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)容也就不會改變鉴逞。

swap之后

StatefullWidget 結(jié)合 Key

現(xiàn)在华蜒,為 StatefulColorfulTile 傳遞一個 Key 對象:

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

再次運行:

PositionedTiles

成功 swap!

添加了 Key 之后的結(jié)構(gòu):

PositionedTiles

當現(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 匹配算法將一次只檢查樹的一個層級:

檢查
  1. 在第一級肿仑,Padding Widget 都正確匹配。
檢查
  1. 在第二級勾邦,F(xiàn)lutter 注意到 Tile 控件的 Key 不匹配眷篇,就停用該 Tile Element荔泳,刪除 Widget 和 Element 之間的連接
檢查
  1. 我們這里使用的 KeyUniqueKey玛歌, 它是一個 LocalKey

LocalKey 的意思是: 當 Widget 與 Element 匹配時,F(xiàn)lutter 只在樹中特定級別內(nèi)查找匹配的 Key创肥。因此 Flutter 無法在同級中找到具有該 Key 的 Tile Widget值朋,所以它會創(chuàng)建一個新 Element 并初始化一個新 State昨登。 就是這個原因,造成色塊顏色發(fā)生隨機改變撒强,每次交換相當于生成了兩個新的 Widget笙什。

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

當 Widget 樹中兩個 Padding 發(fā)生交換之后琐凭,F(xiàn)lutter 就能根據(jù) PaddingKey 的變化,更新 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(),
     ),
   ];
 }
根據(jù) Padding Key

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
  • 在第二種情況下蓝仲,您可能希望驗證密碼官疲,但不希望與樹中的其他 Widget 共享該狀態(tài)信息途凫,可以使用 GlobalKey<FromState> 持有一個表單 FormState。 Flutter.dev 上有這個例子Building a form with validation棚饵。

其實 GlobalKeys 看起來有點像全局變量噪漾。有也其他更好的方法達到 GlobalKeys 的作用且蓬,比如 InheritedWidget恶阴、Redux 或 Block Pattern。

總結(jié)

如何合理適當?shù)氖褂?Key:

  1. When: 當您想要保留 Widget 樹的狀態(tài)時焦匈,請使用 Key缓熟。例如: 當修改相同類型的 Widget 集合(如列表中)時
  2. Where: 將 Key 設(shè)置在要指明唯一身份的 Widget 樹的頂部
  3. Which: 根據(jù)在該 Widget 中存儲的數(shù)據(jù)類型選擇使用的不同類型的Key

參考

上文涉及的例子代碼: https://github.com/stefanJi/fullter-playgroud

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末够滑,一起剝皮案震驚了整個濱河市吕世,隨后出現(xiàn)的幾起案子命辖,更是在濱河造成了極大的恐慌,老刑警劉巖尔许,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件母债,死亡現(xiàn)場離奇詭異尝抖,居然都是意外死亡昧辽,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進店門框咙,熙熙樓的掌柜王于貴愁眉苦臉地迎上來痢甘,“玉大人塞栅,你說我怎么就攤上這事∽餮蹋” “怎么了旱易?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵雳殊,是天一觀的道長蝠检。 經(jīng)常有香客問我涎显,道長期吓,這世上最難降的妖魔是什么倾芝? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任潭千,我火速辦了婚禮借尿,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘路翻。我一直安慰自己狈癞,他們只是感情好,可當我...
    茶點故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布茂契。 她就那樣靜靜地躺著蝶桶,像睡著了一般。 火紅的嫁衣襯著肌膚如雪掉冶。 梳的紋絲不亂的頭發(fā)上真竖,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天脐雪,我揣著相機與錄音,去河邊找鬼战秋。 笑死,一個胖子當著我的面吹牛旁振,可吹牛的內(nèi)容都是我干的获询。 我是一名探鬼主播涨岁,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼拐袜,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了梢薪?” 一聲冷哼從身側(cè)響起蹬铺,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎秉撇,沒想到半個月后甜攀,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡琐馆,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年规阀,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片瘦麸。...
    茶點故事閱讀 38,018評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡谁撼,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出滋饲,到底是詐尸還是另有隱情厉碟,我是刑警寧澤,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布屠缭,位于F島的核電站箍鼓,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏呵曹。R本人自食惡果不足惜款咖,卻給世界環(huán)境...
    茶點故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望奄喂。 院中可真熱鬧铐殃,春花似錦、人聲如沸砍聊。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽玻蝌。三九已至蟹肘,卻和暖如春词疼,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背帘腹。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工贰盗, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人阳欲。 一個月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓舵盈,卻偏偏與公主長得像,于是被迫代替她去往敵國和親球化。 傳聞我的和親對象是個殘疾皇子秽晚,可洞房花燭夜當晚...
    茶點故事閱讀 42,762評論 2 345

推薦閱讀更多精彩內(nèi)容