Flutter佈局指南之誰動了我的Key
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,
),
],
)
展示為兩個不同顏色的方塊。
問題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
@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),
],
)
這樣當我們點選計時器工作之後,展示如下。
問題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 歡迎大家訪問
- 閒言碎語-第八期
- kotlin修煉指南9-Sequence的祕密
- 起點客戶端精準化測試的演進之路
- Flutter混編工程之打通紋理之路
- Android桌布還是B站玩得花
- Flutter佈局指南之誰動了我的Key
- Material Components——ShapeableImageView
- JetPack指路明燈—Navigation
- Material Components—預備役選手Transition
- 靜若處子動若脫兔-Constraintlayout2.0一探究竟
- Kotlin修煉指南5
- 重走Flutter狀態管理之路—Riverpod最終篇
- Material Components——MaterialButton
- ConstraintLayout2.0進階之路-歡迎新同學
- ConstraintLayout使用場景必知必會
- 重走Flutter狀態管理之路—Riverpod進階篇
- 它來了!Flutter3.0新特性全接觸
- 重走Flutter狀態管理之路—Riverpod入門篇
- 它來了!Flutter3.0釋出全解析
- Material Components——Shape的處理