那么熟悉又那么陌生——Flutter Key 究竟是什么

michael-dziedzic-1bjsASjhfkE-unsplash.jpg

一锁蠕、Key

我們平時一定接觸過很多的 Widget惑艇,比如 Container蒿辙、Row、Column 等滨巴,它們在我們繪制界面的過程中發(fā)揮著重要的作用思灌。但是不知道你有沒有注意到,在幾乎每個 Widget 的構造函數(shù)中恭取,都有一個共同的參數(shù)泰偿,它們通常在參數(shù)列表的第一個,那就是 Key秽荤。

image-20200707202436767.png

但是在我們構造這些 Widget 的時候甜奄,又很少指定并傳入wfdh這個參數(shù),那么這個參數(shù)究竟是干嘛的呢窃款?它又有什么作用呢课兄?

我們先來看看 Key 這個類吧,它是個虛擬類晨继,有子類 LocalKeyGlobalKey烟阐,它們也是虛擬類,又有各自的子類紊扬。

image-20200707204453713.png

它們有什么區(qū)別呢蜒茄?具體有什么使用場景呢?帶著這些疑問餐屎,我們繼續(xù)向下看檀葛。

二、煮個栗子

我們首先看一個 demo:有兩個色塊和一個按鈕腹缩,點擊按鈕后屿聋,兩個色塊會進行交換。

valid_swap.gif

代碼部分藏鹊,兩個無狀態(tài)的色塊 Widget润讥,點擊按鈕后,交換 Widget 在 List 中的位置并進行刷新盘寡。

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 中,那么情況又如何呢菇曲?此時發(fā)現(xiàn)冠绢,無論我們怎么點擊交換按鈕,色塊的位置或者顏色都不會再交換了常潮。但是如果將顏色的屬性存儲在 Widget 中而不是 State 中弟胀,那么此時的交換效果又變得正常了。這是什么原因呢喊式?

我們知道孵户,F(xiàn)lutter 中 Widget 并不是最終繪制在屏幕上的對象,它只是用來配置和儲存測繪數(shù)據(jù)的一種媒介以及構建真正的圖像和用戶交互的橋梁岔留,而 Element 則是視圖樹的骨架夏哭,標識了各個控件在視圖樹上的一種樹形結構的關系,而真正被繪制到屏幕上的則是 RenderObject献联。

那么竖配,第一個例子中的兩個色塊為什么可以交換呢何址?

stateless_widget.png

左邊是我們布局的視圖結構,framework 會構造出與之一一相對應的 Element 的結構關系进胯。當我們交換兩個色塊后用爪,左邊的 WIdget 關系中,兩個 Widget 的樹型相對位置發(fā)生變化胁镐,此時通過 setState 方法通知 framework 視圖樹可能有變化需要刷新偎血,Element 樹就會被遍歷來和 Widget 樹一一進行對比是否還相等,包括類中的屬性和成員盯漂。當發(fā)現(xiàn)表示顏色的屬性不再相對應相等時颇玷,就可以知道視圖樹發(fā)生了改變,需要進行刷新就缆,Element 樹會重新和 Widget 樹進行對應構造帖渠,屏幕上的兩個色塊交換的效果就會被繪制出來。

stateless_widget_swapped.png

簡而言之竭宰,界面之所以會刷新阿弃,就是因為 framework 發(fā)現(xiàn)了視圖樹產(chǎn)生變化,所以知道自己需要刷新進行重新繪制羞延。其中重點就是 framework 能夠發(fā)現(xiàn)視圖樹的變化渣淳。

那為什么我們換用 StatefulWidget 并將顏色屬性存儲在 State 中后就無法進行交換了呢?原因恰恰就是在于 framework 此時已經(jīng)無法發(fā)現(xiàn)視圖樹產(chǎn)生變化了伴箩。

stateful_widget.png

這種情況下入愧,Widget 和 Element 樹和之前的情況大體相同,但是不同的是嗤谚,現(xiàn)在每個小色塊 Widget 都關聯(lián)一個 State棺蛛,用來管理它們的狀態(tài),而顏色屬性就儲存在 State 中巩步。Element 樹有和 Widget 樹一一對應的 Element 節(jié)點旁赊,但是 Element 并沒有用來管理狀態(tài)的 State。

在我們點擊按鈕進行交換后椅野,同樣的终畅,Widget 的樹中的位置發(fā)生變化,然后 setState 方法通知 framework 可能有視圖樹的變化需要注意竟闪,Element 被指派去和 Widgewfdhwfdhwfdhwfdht 樹進行比對离福,但是由于 StatefulColorBlock 中不存儲任何狀態(tài)值(或者說不存在任何能夠區(qū)別的不相同的狀態(tài)值),所以 Element 遍歷后長抒一口氣:“沒有不一樣的地方炼蛤,不需要進行樹的更新妖爷,繼續(xù)劃水……”,本著偷懶的原則理朋,視圖樹并沒有接收到需要刷新的指令絮识,就不做任何處理工作绿聘,盡管實質(zhì)上它們的 State 已經(jīng)不同,但是這并不能引起刷新機制的注意次舌,因此帶來的結果就是無論我們怎么點擊交換按鈕斜友,界面都紋絲不動,不會進行任何更新垃它。

stateful_widget_swapped.png

至此,對于將顏色屬性存儲在 Widget 中而不是 State 中的交換效果正常的原理也就顯而易見了烹看。

那么国拇,對于上面說的顏色無法交換的情況甚或其他各種類似的情況,我們在開發(fā)中該怎么處理呢惯殊?

就在此時酱吝,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 樹的時候冲甘,發(fā)現(xiàn)了無法對應的 Key,所以判定視圖樹的結構發(fā)生了變化途样,有必要進行刷新江醇,所以會對兩個色塊進行重繪,所以我們就能看到色塊交換的效果啦何暇。

三陶夜、品,你細品

flutter 源碼在 Key 類上的注釋文檔開門見山地說明了裆站,Key 是 Widget条辟、Element 和 SemanticsNode 的“身份證”。幫助它們進行l(wèi)wlwlwlwlw有效的區(qū)分宏胯。而且 Key 是視圖樹的更新策略的重要依據(jù)捂贿。下面摘錄 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 在視圖樹上的替換規(guī)則厂僧。如果兩個 Widget 的 runtimeType 和 Key 都是相等的(用 == 操作符比較結果為真),那么新的 Widget 會通過更新其下的 Element(通過調(diào)用 Element.update 并傳入新的 Widget 的方式)了牛。否則颜屠,舊的 Element 就會被從視圖樹上刪除辰妙,新的 Widget 掛載到新的 Element,新的 Element 再被插入視圖樹上甫窟。

另外密浑,如果用 GlobalKey 作為 Widget 的 Key,能夠使該 Widget 在整個視圖樹上移動(其父節(jié)點會發(fā)生變更)而不丟失狀態(tài)粗井。如果有一個新的 Widget尔破,它和原先在此位置的 Widget 的 Key 或者類型不相同,但是在前一幀有一個和它相同的 GlobalKey 的 Widget浇衬,位置在其他地方懒构,那么那個前一幀的 Widget 所依附的 Element 就會移動到新的 Widget 所在的視圖樹的位置。

對 Key 的工作原理有了一個大致的了解耘擂,那么我們再詳細看看 Key 的那些實現(xiàn)的子類們胆剧。

  • UniqueKey

    只和它自己判定相等的 Key,它不能被 const 修飾構造函數(shù)醉冤,因為如果被 const 關鍵字修飾秩霍,那么它的所有實例都將是同一個,違背只能和它自己相等的原則蚁阳。

  • ValueKey

    可以指定其值的 Key铃绒,其值被泛型約束。當且僅當兩個 ValueKey 能被 == 操作符判定相等螺捐,它們才是相等的匿垄。

  • PageStorageKey

    它是 ValueKey 的子類,也可以指定一個被泛型約束的值归粉。正如其名椿疗,它和 PageStorage 類密切相關。PageStorage 是一個保存和恢復值的 Widget 子類糠悼。而 PageStorageKey 正是用來在 widget 重建之后找回和恢復存儲的值届榄。在每個路由中,有一個存儲數(shù)據(jù)的 Map倔喂,而該 Map 的 Key 正是由 PageStorageKey 所定義的值來決定的铝条。所以,PageStorage 創(chuàng)建時所傳入的值不應該隨著 widget 的重建發(fā)生變化席噩。

  • ObjectKey

    ObjectKey 和 ValueKey 很類似班缰,也是傳入一個值,并通過其值來比較二者是否相等悼枢。但是它們有兩個不同點埠忘。

    1. ObjectKey 的 value 不是泛型約束的,而是一個 Object 對象;

    2. 二者的重載操作符方法 == 內(nèi)容不一樣莹妒。

      // 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 的子類之前鉴腻,我們先來看看這個雖然是虛擬類但其實比起其實現(xiàn)的子類可能更重要的父類迷扇。下面摘錄 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 在從原來的位置被刪除到在新的位置“扎根”需要在一個動畫幀內(nèi)完成踢步。

GlobalKey 是高昂的癣亚,如果你不是真的需要它,盡量考慮使用 Key获印、ValueKey述雾、ObjectKey 或 UniqueKey 代替。

視圖樹上的兩個 widget 不可能同時擁有同樣的 GlobalKey兼丰,如果嘗試如此玻孟,會無法通過 assert 語句的斷言。

我們這里需要注意三點:

  1. GlobalKey 在整個 app 都是唯一的鳍征;

  2. 指定 GlobalKey 的對象黍翎,能夠在整個視圖樹上的任意節(jié)點間移動(而不丟失狀態(tài));

  3. GlobalKey 能夠獲取 element 的相關類艳丛。

我們著重解釋3匣掸,因為2就是3的結果。

注意到 GlobalKey 中有一個靜態(tài)對象 static final Map<GlobalKey, Element> _registry = <GlobalKey, Element>{}氮双,它是 GlobalKey 和 Element 的鍵值對碰酝,而其數(shù)據(jù)的填充是在 _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() 中被調(diào)用戴差。所以通過這個存儲下來的 element送爸,GlobalKey 就可以拿到所有與其相關的對象——BuildContext、Element、Widget 以及 State 等碱璃。

好了弄痹,聊了這么多的 GlobalKey,下面看看它的兩個子類嵌器。

  • LabeledGlobalKey

    在 GlobalKey 的基礎上增加了一個 _debugLabel 屬性肛真,用來在 log 等輸出做調(diào)試用,而且 GlobalKey 的工廠構造函數(shù)返回的其實就是 LabeledGlobalKey爽航。

  • GlobalObjectKey

    聯(lián)想 ObjectKey 和 LocalKey 的關系蚓让,它可以看做自己指定 value 的 GlobalKey,但是這就可能造成因指定了相同的 value 而導致的沖突問題讥珍,解決方法是構造其私有的子類历极。

    class _MyKey extends GlobalObjectKey {
       const _MyKey(Object value) : super(value);
    }
    

四、適合的才是最好的

經(jīng)過上面對各種 Key 的介紹衷佃,其各自適用場景也很明顯了趟卸。因為 GlobalKey 代價高昂,所以除非在一些需要在不同視圖樹節(jié)點同步 widget氏义、element 和 State 的屬性的情況下锄列,盡量使用 LocalKey,而各個 LocalKey 的不同特性又決定了它們不同的使用場景惯悠。

Key 在整個 flutter 的結構體系中很小邻邮,我們對他最陌生又最熟悉。搞懂它的作用克婶,那我們在下次點開 widget 的構造函數(shù)時筒严,發(fā)現(xiàn)躺在參數(shù)列表第一個的 Key,我們也可以微微一笑:我認識你情萤。

(以上部分內(nèi)容來自Flutter 官方 YouTube 介紹視頻和 Flutter 源碼注釋文檔)

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末鸭蛙,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子筋岛,更是在濱河造成了極大的恐慌规惰,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件泉蝌,死亡現(xiàn)場離奇詭異歇万,居然都是意外死亡,警方通過查閱死者的電腦和手機勋陪,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進店門贪磺,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人诅愚,你說我怎么就攤上這事寒锚〗儆常” “怎么了?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵刹前,是天一觀的道長泳赋。 經(jīng)常有香客問我,道長喇喉,這世上最難降的妖魔是什么祖今? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮拣技,結果婚禮上千诬,老公的妹妹穿的比我還像新娘。我一直安慰自己膏斤,他們只是感情好徐绑,可當我...
    茶點故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著莫辨,像睡著了一般傲茄。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上沮榜,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天盘榨,我揣著相機與錄音,去河邊找鬼敞映。 笑死较曼,一個胖子當著我的面吹牛磷斧,可吹牛的內(nèi)容都是我干的振愿。 我是一名探鬼主播,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼弛饭,長吁一口氣:“原來是場噩夢啊……” “哼冕末!你這毒婦竟也來了?” 一聲冷哼從身側響起侣颂,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤档桃,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后憔晒,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體藻肄,經(jīng)...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年拒担,在試婚紗的時候發(fā)現(xiàn)自己被綠了嘹屯。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,137評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡从撼,死狀恐怖州弟,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤婆翔,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布拯杠,位于F島的核電站,受9級特大地震影響啃奴,放射性物質(zhì)發(fā)生泄漏潭陪。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一纺腊、第九天 我趴在偏房一處隱蔽的房頂上張望畔咧。 院中可真熱鬧,春花似錦揖膜、人聲如沸誓沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽拜隧。三九已至,卻和暖如春趁仙,著一層夾襖步出監(jiān)牢的瞬間洪添,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工雀费, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留干奢,地道東北人。 一個月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓盏袄,卻偏偏與公主長得像忿峻,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子辕羽,可洞房花燭夜當晚...
    茶點故事閱讀 42,901評論 2 345

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

  • Key在Flutter的源碼中可以說是無處不在逛尚,但是我們?nèi)粘V写_不怎么使用它。有點像是“最熟悉的陌生人”刁愿,那么今天...
    唯鹿_weilu閱讀 1,177評論 2 3
  • 前言 在開發(fā) Flutter 的過程中你可能會發(fā)現(xiàn)绰寞,一些小部件的構造函數(shù)中都有一個可選的參數(shù)——Key。剛接觸的同...
    Vadaski閱讀 6,834評論 15 53
  • 新創(chuàng)建一個Flutter Application的時候铣口,默認生成的代碼里面有這么一段 title很好理解滤钱,給App...
    whqfor閱讀 852評論 0 0
  • 前言 Flutter 中一切皆 Widget,而 Widget 的構造方法中有個可選參數(shù) Key脑题。一般情況下我們不...
    teletian閱讀 2,386評論 0 5
  • 概述 在Widget的構造方法中件缸,有Key這么一個可選參數(shù),Key是一個抽象類旭蠕,有LocalKey和GlobalK...
    iOSer_jia閱讀 1,512評論 0 5