Flutter佈局指南之誰動了我的Key

語言: CN / TW / HK

Key用來幹嘛

Flutter中的Key,一直都是作為一個可選引數在很多Widget中出現,那麼它到底有什麼用,它到底怎麼用,本篇文章將帶你從頭到尾,好好理解下,Flutter中的Key。

我們首先來看下面這個Demo:

Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Container( width: 100, height: 100, color: Colors.red, ), Container( width: 100, height: 100, color: Colors.blue, ), ], )

image-20220227203558343

展示為兩個不同顏色的方塊。

問題1

這時候,如果我們在程式碼中交換兩個Container的位置,Hot reload之後,它們的位置會發生改變嗎?

下面我們把Demo修改一下,將Container抽取出來,並在中間放一個Text用來做計時器,並改為StatefulWidget,程式碼如下。

``` class KeyBox extends StatefulWidget { final Color color;

KeyBox(this.color);

@override _KeyBoxState createState() => _KeyBoxState(); }

class _KeyBoxState extends State { var counter = 0;

@override Widget build(BuildContext context) { return Container( width: 100, height: 100, color: widget.color, child: Center( child: TextButton( onPressed: () { setState(() => counter++); }, child: Text( counter.toString(), style: const TextStyle(fontSize: 60), ), ), ), ); } } ```

Column( mainAxisAlignment: MainAxisAlignment.center, children: [ KeyBox(Colors.yellow), KeyBox(Colors.green), ], )

這樣當我們點選計時器工作之後,展示如下。

image-20220227203642652

問題2

這時候,如果我們在程式碼中交換兩個Container的位置,Hot reload之後,它們的數字會發生改變嗎?

問題3

如果我們刪掉第一個Widget,Hot reload之後,顯示的是數字幾?

問題4

如果我們再重新把刪掉的Widget加回來,Hot reload之後,又會如何顯示?

問題5

如果在問題2的基礎上,給第一個Widget外新增一個Center,那麼又會如何顯示呢?

如果你能完全回答上面的這幾個問題並知道為什麼,那麼恭喜你,看完這篇文章,你會浪費十幾分鍾,當然,如果你不清楚,那麼這十幾分鐘的時間,將給你帶來不小的收益。

Key是什麼

Flutter通過Widget來渲染UI,那麼它是如何區分上面的兩個不同顏色的Container的呢?通過顏色嗎?當然不是,如果Container的顏色相同,那豈不是無法區分了?

所以,Key就成了Flutter區分不同Widget的依據,這就好比是Android中佈局的ViewID。

知道Key是什麼還不夠,我們還得知道,我們為什麼需要Key,首先,我們來看下上面的三個問題。

對於問題1,這個應該很簡單了,Container是StatelessWidget,所以每次Hot reload都會重新build,因此顏色肯定會發生互換,這個很好理解。

那麼對於問題2呢?StatelessWidget改成了StatefulWidget,這次再交換兩個Widget的位置,你可以發現,雖然顏色互換了,但是數字沒變。

要怎麼解決這個問題呢?這就需要用到Key了,我們給KeyBox增加一個Key的引數。

新的Flutter Lint已經會提示你建構函式需要增加key的可選引數了。

const KeyBox(this.color, {Key? key}) : super(key: key);

在使用的地方,傳入ValueKey即可。

KeyBox(Colors.yellow, key: ValueKey(2)), SizedBox(height: 20), KeyBox(Colors.cyan, key: ValueKey(1)),

這時候你再切換兩個Container的位置,數字就會跟著變換了。

Key的原理

Key實際上是Flutter用來標記Widget的唯一標識,但是為什麼需要Key,就要從Flutter的渲染流程上說起了。

Widget作為Flutter中的不可變資料,是作為渲染的資料類而存在的,它實際上就是內容的配置表,根據View的樹形結構,自然而然模擬出了一個Widget Tree的概念。

Widget在執行時會建立Element例項,這些Element和Widget也組成了一一對應的關係,對於StatefulWidget來說,Widget中包含了元件的外觀、位置等資訊,而Element中,包含了State資訊,這也是Flutter的核心原理。所以,在上面的Demo中,Counter作為State,被儲存在Element中,而顏色,被儲存在Widget中。

Widget和Element分離之後,如果修改顏色等Widget屬性,那麼可以直接建立新的Widget替換舊的Widget,同時還可以保留Element中的資料,因為建立Widget的成本是很低的,而Element則會高很多,所以Element會持續儘可能長的時間。

那麼在Widget被改變之後,Element是如何和Widget進行關聯的呢?這就需要兩個東西了:

  • runtimeType
  • Key

所以Element會先對比當前新的Widget Tree中的新元素,是否跟當前Element的型別一致,如果不一致,那麼說明Element已經無效了,只能重新建立,如果型別一致,那麼就需要進一步判斷Key了。

問題2的原因

所以,在問題2中,由於兩個Widget的型別並沒有發生變化,而又沒有Key,所以,Widget被重新建立後,與原來的Element又關聯起來了,看上去就是隻修改了顏色。

那麼在問題2的解法中,我們給Widget增加了Key,當我們調換兩個Widget的位置時,雖然型別沒有改變,但是Key發生了改變,Element在原來的位置找不到對應的Widget,那麼這時候,它會選擇在當前層級下,繼續搜尋這個Key。

這裡要注意,Element只會在當前層級下搜尋,如果這個Key的Widget被移入了其它層級,那麼也是無法找到的,在問題2的場景下,由於只是交換了兩個Widget的順序,所以Element會在後面找到之前Key的Widget,同理,下一個Element也會找到,所以,兩個Widget都被關聯起來了,所以State也顯示正確了。

問題3的原因

那麼在問題3中,我們刪除了第一個Widget,當沒有Key時,Element會在Widget Tree中搜索,當它發現第二個Key型別是一樣的時,它就以為它找到了,而第二個Element,因為找不到Widget,就銷燬了。最終的效果就是剩下第二個Box的顏色和第一個Box的數字。

那麼如果有Key呢?有Key的話,就不會找錯了啊,所以自然能夠對應上,與我們預想的也就是一樣的了。

問題4的原因

理解了問題3,那麼問題4就好理解了。當我們在開頭建立同一個型別的Widget時,Element會把這個新增的Widget當作是以前的Widget,因為它們型別相同,所以Element被關聯到了這個新的Widget,而另一個Widget發現已經沒有Element了,所以會選擇新建一個Element,這時候,數字就是預設值0了。

問題5的原因

對於問題5來說,實際上就是Element的搜尋機制,前面解釋了,Element只會在當前層級進行搜尋,所以Center的加入,改變了Widget的層級,Element無法對應了,所以它也選擇了消耗重建,所以第一個Widget會顯示預設值0。

但是要注意的是,如果型別不一致,那麼Flutter會直接判斷不相同,從而直接消耗重建,所以,在這些問題裡,如果在KeyBox之間插上一些不同型別的Widget,那麼就瞬間破防了,演示的效果就完全不同了。

Key有哪些Key

Key從整體上來說,分為兩種,即:

  • Local Key:分為Value Key、Object Key和Unique Key
  • Global Key

Local Key顧名思義,指的是在當前Widget層級下,有唯一的Key屬性,而Global Key,則是在全域性APP中,具有唯一性。Global Key的效能會比Local Key差很多。

Value Key

在前面的Demo中,我們給KeyBox增加了Key之後,Widget在修改、移動之後,Element就可以正確的找到對應的Widget了,這裡我們使用的是Value Key。

Value Key,顧名思義,就是使用Value來對Key做標識的Key,例如我們在Demo中使用的,ValueKey(1),value可以是任意型別,這裡是1,其實更符合的場景,應該是用Color,或者是更加具有語義性的value來作為Key的value。

Value Key在同一層級下需要具有唯一性,所以當兩個KeyBox都設定成ValueKey(1)時,程式就會報錯,告訴你Key重複了。

Object Key

Object Key與Value Key類似,但是又不完全一樣,Value Key對比的是Value,Value相等,就是相等,而Object Key,對比的是例項,例項相同,才是相等,就好比一個Java中的equals,一個是「==」。我們看下Object Key的原始碼就一目瞭然了。

@override bool operator ==(Object other) { if (other.runtimeType != runtimeType) return false; return other is ObjectKey && identical(other.value, value); }

假如我們有一個自定義的Class,重寫了它的==函式,那麼用Value Key,new兩個同樣的物件,它們就是相等的,而Object Key,則不相等,原因就是一個比較的是值,一個比較的是引用。

Unique Key

Unique Key自己都說了,它是獨一無二的,也就是說,Unique Key只和自己相等,任意建立多個Unique Key,都是不相等的,相當於唯一標識了。

如果在Build函式中建立Unique Key,那麼這個Key在大部分場景下就沒有意義,因為Hot reload時,Build函式會重建,所以Unique Key被重建,而且和之前也不相等。

這就很奇怪了,這玩意有什麼用呢?

用處確實不多,但一旦用到,就必須得用,例如下面這個例子。

假如我們要用AnimatedSwitcher來實現切換時的動畫效果,這時候,我們需要讓每次改變都要執行動畫,那麼這裡就可以使用Unique Key,強制每一次都是新的Widget,這樣才能有動畫效果。

那麼另一種使用場景,就是在無法使用Value Key和Object Key的時候使用,但是這時候,需要將Unique Key定義在Build函式之外,這樣Unique Key只會建立一次,從而保證唯一性的同時,不用去建立value和Object。

Global Key

Global Key全域性唯一且只和自己相等,還記得之前Element在關聯新變化的Widget時是怎麼比較Key的嗎——Element為了效率問題,只會在當前層級下進行尋找,所以,在問題5中,一旦我們修改了某個Widget的層級,那麼Element就會消耗重建,那麼如果使用了Global Key呢?當Key的型別是Global Key時,Element會不惜代價在全域性尋找這個Key,這也是為什麼Global Key的效率會比較低的原因。

那麼有了Global Key,即使Widget Tree發生了改變,也依然可以找到這個Widget進行關聯,但是要注意的是,Global Key需要定義在Build函式之外,否則每次都會重新建立Global Key,那就沒有意義了。

除此之外,Global Key還有一個作用,那就是給一個Widget增加一個全域性標識,這樣有點像指令式程式設計的意思,類似Android中的FindViewByID,通過Global Key就可以找到當前標記的這個Widget,從而獲取它的一些相關資訊。

``` final count = (globalKey.currentState as _KeyBoxState).counter; print('count: $count'); final color = (globalKey.currentWidget as KeyBox).color; print('color: $color'); final size = (globalKey.currentContext?.findRenderObject() as RenderBox).size; print('size: $size'); final position = (globalKey.currentContext?.findRenderObject() as RenderBox).localToGlobal(Offset.zero); print('position: $position');

// output flutter: count: 0 flutter: color: MaterialColor(primary value: Color(0xff4caf50)) flutter: size: Size(100.0, 100.0) flutter: position: Offset(145.0, 473.5) ```

由此可見,通過Global Key,我們可以拿到State、Widget、Element(Context)以及通過Element關聯的RenderObject,這樣就可以獲取Widget中的一些配置引數,State中的資料變數,以及RenderObject中的繪製資訊,例如尺寸、位置、約束等等。

向大家推薦下我的網站 https://xuyisheng.top/ 專注 Android-Kotlin-Flutter 歡迎大家訪問