Flutter 熟悉的陌生人Key 的原理和使用(wei.zhou小為)

語言: CN / TW / HK

highlight: a11y-light

每一個Widget都有一個可選傳遞的引數key,我們一般不會使用到它,但是在一些特定或者複雜的場景下,它必須出場,扮演著重要的角色,我們一起來學習認識一下它.

一.沒有Key會發生什麼事情

我們建立一個具有狀態的Widget(Box)

```

class Box extends StatefulWidget { final Color? color; Box(this.color, {Key? key}) : super(key: key);

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

class _BoxState extends State { int count = 0;

@override Widget build(BuildContext context) { return GestureDetector( onTap: (){ setState(() { count++; }); }, child: Container( width: 150, height: 150, color: widget.color, child: Center( child: Text('$count',style: const TextStyle(fontSize: 50),), )), ); } }

``` 大家可以看到,裡面的數字是在state裡面,我們手指點選數字就會加一。 然後現在看看主程式的程式碼:

``` class _KeyShareDemoState extends State {

@override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.title ?? ''), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Box(Colors.red), Box(Colors.yellow), Box(Colors.blue), ], ), ), ); } } ```

初始狀態:

我們放在同一層級的Column下面,然後第一個Box點選一下,第二個Box點選兩下,第三個Box點選三下,執行結果如下圖

截圖2022-06-19 下午12.22.40.png

顏色順序為紅、黃、藍,數字分別為1、2、3.

情景一

現在,我們人為的將第一個Widget和第三個Widget的程式碼順序進行對調,我們猜想:此時的顏色和數字都會進行對調,是嗎?結果如下圖:

Simulator Screen Shot - iPhone 13 - 2022-06-19 at 12.32.12.png

我們發現:顏色是對調了,但是裡面的數字還是原來的數字,令人費解.

情景二:

我們回到初始化的狀態,還是紅、黃、藍的三個按鈕,數字還是1、2、3,我們現在人為的去掉第一個紅色的Widget,那麼我們猜想是不是應該留下數字2和3,數字1 不見了,顏色剩下黃色和藍色呢?執行結果如下.

Simulator Screen Shot - iPhone 13 - 2022-06-19 at 12.57.34.png

我們發現,顏色每次都是對的,沒有讓我們失望,但是這個數字1居然留下了,數字3不見了,我們更加疑惑了.

這個時候系統其實已經分不清楚誰是誰了,因為它們處於同一層級,又是相同的型別。所以,我們需要給它們一個Key作為它們各自的唯一識別符號.如下圖:

截圖2022-06-19 下午3.25.31.png

我們給了它們每個人一個ValueKey,並且每一個都給了不同的值,我們就發現,無論我們是調換順序也好,還是刪除其中一個Widget也好,它們都會如我們所願,不會亂了.

結論:在同一層級樹中(注意不同層級另當別論,這裡的同一層級指的是三個Box都在同一個Column下面),Key可以作為唯一標識,標記我們的Widget,系統在複用的時候才不會混亂.

情景三

我們現在有Key了,都是ValueKey.我在第三個Box外面套上一層Center這個小部件,依然保留ValueKey.如下圖:

截圖2022-06-19 下午3.40.55.png

這樣加上了Center過後,然後點選熱更新,事情再次沒有如我們所願,我們的第三個Box,剛開始是3,現在變成了0.但是顏色依然沒有問題!

Simulator Screen Shot - iPhone 13 - 2022-06-19 at 15.49.31.png

情景四

我這裡就不進行演示了,大家可以去進行嘗試,在同一層級下的Container和Text,你調換位置和刪除,都會如你所願,不會混亂。因為它們都是StatelessWidget,他們沒有狀態state.我們在幾次的試驗中也發現,顏色處於widget側,它始終是沒有問題的,出問題的是狀態。

二.Widget樹與Element樹的對應關係

為了解釋上面的奇異現象,我們必須去了解與學習Widget樹與Element樹的原理與關係。如下圖所示:

截圖2022-06-19 下午7.06.03.png

1.Widget樹:我們平時寫程式碼的地方,它是一個藍圖,是一份描述UI元素Element的配置的描述檔案.

2.Element樹:真正生成檢視物件和狀態的樹。它與widget是一一對應的關係,每個widget都會呼叫其內部的 createElement方法,從而例項化生成Element物件。如下圖,為Widget的原始碼.

截圖2022-06-19 下午4.22.01.png

3.State是跟著Element物件走的,並不在Widget側。Widget負責如何渲染,比如顏色,大小,形狀等等,而Element負責管理裡面的狀態。所以狀態是隨著Element來改變的,外觀是隨著Widget改變的.二者是分開的。

4.Widget和Element為什麼要分開呢? 因為Widget是不可變的!可以改變的是State狀態,當狀態發生改變後,flutter會去重建一個新的widget,去替換掉舊的widget,而不是去改變這個widget,因為widget是不可變的.

5.我們在上面的例子中,交換兩個Box的位置,或者去掉一個Box時,它們所對應的Element並不一定被調換了順序,或者說被正確的刪除了。這才是問題的關鍵所在.如圖所示:

截圖2022-06-19 下午6.24.33.png

這是一個Widget裡面的原始碼方法,canUpdate方法會對新舊widget的型別和key進行判斷,如果它們都相等,系統就認為它們可以更新。

意思是:一旦這個方法判定成立,Widget與Element物件就可以進行對應!可以進行關聯!那麼一旦判斷錯誤,該Widget就會錯誤的與Element和State進行關聯。上面我舉出的例子,就是發生了這種情況!

三.覆盤解釋上面的現象

1.widget順序交換問題.

無key:

當我們調換順序時,系統重走canUpdate方法進行判定,由於在同一層級下,全部都是Box型別的Widget,並且我們沒有給到key,key都為null,null==null。那麼,此判斷條件就成立了,返回true。也就是說,以前的Element1就關聯上了交換順序後的widget3,由於Element1的狀態為1,所以第一個方塊還是顯示的1.同理可得,由於我們沒有給Key,以前的Element3就關聯上了交換順序後的widget1,Element3的狀態為3,那麼第三個方塊還是顯示的3.由此可見,雖然我們交換了widget的順序,但是Element的順序並不一定會跟著發生改變,一切以canUpdate判定結果為準.

有key:

比如現在我們交換的是Box2與Box3的順序,系統在檢索Box3與Element2的時候,由於我們給了Key,那麼canUpdate方法將返回false,他們將不會建立聯絡,如圖所示:

截圖2022-06-19 下午7.46.19.png

不能建立對應聯絡的話,它會繼續進行同級別檢索(注意這裡我還是說的同級,跨級我後面會說明),同級檢索到可以建立對應聯絡的Box2後,它們就會建立正確的對應關係了,如圖:

截圖2022-06-19 下午7.54.02.png

到了最後,每個widget與Element都建立了正確的對應關係,那麼對應的狀態也跟著走了,數字也正確了,這樣也就達到了我們的目的.如圖所示:

截圖2022-06-19 下午7.57.39.png

2.例子中刪除widget的問題

有了上面的詳細解釋,這個由一張圖概括:

截圖2022-06-19 下午8.06.00.png

我們刪除掉第一個widget後,由於我們沒有給Key,根據canUpdate方法的判定,Box2會與之前的Element1對應,Box3與之前的Element2對應,所以數字1和2被保留。而Element3由於在同級中沒有檢索到能與之匹配的Widget,那麼Element3物件和State都會一併沒銷燬.

3.例子中有給Key,套上Center導致的問題.

可能你也發現了,開始的型別是Box,我們雖然給了Key,但是我們套上Center過後,這個型別就變了。canUpdate型別判斷就不會成功。那麼ELement3由於在同層級無法對應,就會被銷燬,系統會重新生成新的與Center對應的Element物件,那麼狀態也被重置了。所以數字就是初始化的狀態0.

4.StatelessWidget不需要使用Key?

我們開發中常見的Container、Text都是StatelessWidget,它是沒有狀態State的。比如在一個Column中,我們寫兩個Container,我們不傳入key。現在我們交換他們的位置,系統會只比較它們的 runtimeType。這裡 runtimeType 一致,canUpdate 方法返回 true,兩個 Widget 被交換了位置,但是此時這兩個 Element 將不會交換位置,Element呼叫新持有Widget的build方法重新構建,所以widget得到更改,位置交換。UI的內容,都在widget側,所以在同一層級樹中,不需要使用Key.

三.幾種Key的介紹使用.

我們主要有兩大類的Key需要了解.分為區域性鍵與全域性鍵。

  • LocalKey 區域性鍵,在同一級中要唯一,可以理解為同級唯一性
  • GlobalKey 全域性鍵 , 在整個App中必須是唯一的.

區域性鍵的效能是比全域性鍵快的多,因為它只是在同級中搜索比較,全域性鍵在整個工程中都是唯一的,所以它更慢,當然它更加的強大.

截圖2022-06-19 下午9.18.18.png

區域性鍵:

剛剛我們演示的程式碼都在一個Colunm之中,他們處於同一層級,那麼我們就使用的區域性鍵中的ValueKey。

同一層級中,鍵必須是唯一的,不然系統就會報錯崩潰。比如我們的Column、Row、Stack、TabarView,它們的子部件都是同一層級,如果程式出現異常,你就要進行思考是否要使用Key了。

同一層級,使用相同型別的Widget,這個時候子Widget的狀態資料需要更新(比如搜尋功能),這是Key的一個實戰使用的場景

1.ValueKey : 我們可以看到,裡面是一個範型.我們傳什麼都可以,但是注意,它比較的是值!它的使用,我們在上面的例子中已經展示.

截圖2022-06-19 下午9.43.07.png

2.ObjectKey : 它和ValueKey的區別就在於,它比較的是物件,而不是值.如圖:

WeChatd0ea6b176086851260590b118b457d0e.png

可以看到它比較的是Object了,不再是一個範型了。這是這兩個Key唯一的區別.

那麼我們現在定義一個叫做People的物件出來,如下所示.

``` class People{ final String name; final int age;

People(this.name, this.age);

@override bool operator ==(Object other) => identical(this, other) || other is People && runtimeType == other.runtimeType && name == other.name && age == other.age;

@override int get hashCode => name.hashCode ^ age.hashCode; } ```

我現在把它用到ValueKey上面,我故意把它們的值定義為相同的,這樣子系統就可以報錯出來,因為同一層級不能使用相同的Key。如圖所示,系統果然在提示我說,你使用了重複的Key,那麼這兩個ValueKey就是想等的.

WechatIMG160.jpeg

我現在使用ObjectKey,程式執行正常,沒有報錯,系統不認為它們兩相等,因為它在比較兩個物件是否相等.

截圖2022-06-19 下午10.47.50.png

3.UniqueKey

總結:我們前面提到的ValueKey和ObjectKey,還有我們後面即將介紹的GlobalKey,它們有一個共同點就是,它們會幫助我們保持狀態State,與其說是保持狀態,還不如說是保持了Element物件(因為State是跟著Element走的嘛),因為這些Key自己不會變,所以在canUpdate方法判定中,能夠判斷成功,判定成功的話,Element就能夠與Widget對應上來,所以狀態State就被保留下來。

但是UniqueKey不一樣,它是與自身進行比較,並且它每一次都不一樣,它自己會變.每一次的UniqueKey()和UniqueKey()它是不相等的。每一次重新整理頁面,它都是獨一無二的,那麼canUpdate方法永遠都不會判定成功,widget與Element永遠不會對上,Element物件每次都會被重新建立,與之一體的State也一同被重新構建,狀態會被重置!每次都會被重置!換句話說就是:我們在重新整理頁面的時候,UniqueKey主動為我們丟失了狀態, 讓狀態回到原點.

WechatIMG161.jpeg

4.GlobalKey

1.全域性鍵,它依然可以為我們保持狀態State,與區域性鍵不同的是,它可以為我們跨越層級的保持widget的狀態。因為它是在整個App的樹中進行索引查詢的,所以它的速度更慢,但是也正因如此,可以實現跨層級保持ELement.

``` final GlobalKey _globalKey= GlobalKey();//不同其他的Key,它需要進行初始化被持有.

```

WechatIMG162.jpeg

2.GlobalKey為我們提供了幾個主要的方法,讓我們既可以訪問到Widget樹的東西,也可以訪問到Render樹的東西,當然也可以訪問到Element樹的東西.

2.1 獲取對應的widget

onPressed: () { final widget = _globalKey.currentWidget as Box; print(widget.color); } 此處的Widget型別是Box型別,所以需要轉換一下,我們當時在Box的Widget側定義了一個color屬性,我們就可以通過這種方式訪問到它.

2.2 獲取對應的context和RenderObject

context其實就是Element呀,通過它也可以訪問到RenderObject。

final renderBox = _globalKey.currentContext!.findRenderObject() as RenderBox; 我們通過RenderBox就可以獲取到它的尺寸和座標了.

2.3 獲取對應的State狀態

我們定義一個GlobalKey,我們順帶可以給上它需要定位的state狀態型別為_BoxState型別。

截圖2022-06-27 下午10.55.18.png

如圖所示,我們點選三次過後,狀態數字為3.可以通過GlobalKey獲取到_BoxState,並打印出該count值

1871656342276_.pic.jpg

四.什麼時候會使用Key?業務場景是什麼

  • 搜尋功能 : 可能用到,當搜尋不通內容的時候,給出不同的結果,你可能需要改變狀態,重新構建Widget等, 這個需要看你的需求和實現方式.
  • ValueKey:對列表ListView中item進行滑動刪除、調換順序、增加等的時候需要用到。
  • ObjectKey:如果你有一個電話本應用,它可以記錄某個人的電話號碼,並用列表顯示出來,同樣的還是需要有一個滑動刪除操作.我們知道人名可能會重複,這時候你無法保證給 Key 的值每次都會不同。但是,當人名和電話號碼組合起來的 Object 將具有唯一性

好了,以上!