以前入門一些Flutter應用的時候,老是會遇到GlobalKey
這個類,當時我只從代碼的語法上感知到這個東西確定是用來綁定某些東西的,但至於key這東西是啥?爲何要綁定?不綁定的話會怎麼樣?爲何有的Widget
實現須要綁定有有的不須要?這些通通都不知道。html
因而趁着端午有時間,就認真翻了下官方文檔,發現官方文檔說得很是詳細(前提是你對Flutter的控件樹有必定理解),上面的問題基本都回答到,惋惜的是官方是用視頻(YouToBe)講解的,這不便於忘記的時候速讀翻閱,因而我就整理成這篇博客順便加固下印象。java
- 官方doc:api.flutter.dev/flutter/fou…
- 若是你不瞭解Flutter的控件樹:juejin.cn/post/684490…
- 本文demo代碼:github.com/mimajiushi/…
- 強烈建議先了解Flutter的三顆樹知識,否則有些邏輯你可能會以爲很繞。
key的做用是:控制weidget樹上的widget是否被替換(刷新)git
若是兩個weidget的runtimeType和key屬性相等(用==比較),那麼本來指向舊weidge的element,它的指針會指向新的widget上(經過Element.update方法)。若是不相等,那麼舊element會從樹上移除,根據當前新的widget從新構建新element,並加到樹上指向新widget。github
咱們能夠看下代碼是否是這麼回事: Element.update
api
@mustCallSuper
void update(covariant Widget newWidget) {
// This code is hot when hot reloading, so we try to
// only call _AssertionError._evaluateAssertion once.
assert(_lifecycleState == _ElementLifecycle.active
&& widget != null
&& newWidget != null
&& newWidget != widget
&& depth != null
&& Widget.canUpdate(widget, newWidget));
// This Element was told to update and we can now release all the global key
// reservations of forgotten children. We cannot do this earlier because the
// forgotten children still represent global key duplications if the element
// never updates (the forgotten children are not removed from the tree
// until the call to update happens)
assert(() {
_debugForgottenChildrenWithGlobalKey.forEach(_debugRemoveGlobalKeyReservation);
_debugForgottenChildrenWithGlobalKey.clear();
return true;
}());
_widget = newWidget;
}
複製代碼
進入上面的Widget.canUpdate
markdown
static bool canUpdate(Widget oldWidget, Widget newWidget) {
return oldWidget.runtimeType == newWidget.runtimeType
&& oldWidget.key == newWidget.key;
}
複製代碼
能夠看到判斷邏輯基本與文檔一致,這裏有個值得注意的是:Widget
自己不會調用Widget.canUpdate
,這個方法是由Element
負責調用的,也就是Widget
能不能更新,最終仍是Element
說了算app
相信看到這裏你已經明白key是啥以及它的做用了,but talk is cheap show me the code
,那麼咱們怎麼證實這理論是對的呢?下面就給出了代碼demo。less
下面先舉一個不須要用key的例子,代碼邏輯是,集合的元素順序變動後,控件要跟着變化,代碼以下:dom
import 'dart:math';
import 'package:flutter/material.dart';
void main() {
runApp(new MaterialApp(home: PositionedTiles()));
}
class PositionedTiles extends StatefulWidget {
@override
State<StatefulWidget> createState() => PositionedTilesState();
}
class PositionedTilesState extends State<PositionedTiles> {
List<Widget> tiles;
@override
void initState() {
super.initState();
tiles = [
// StatefulColorfulTile(),
// StatefulColorfulTile(),
// StatefulColorfulTile(key: UniqueKey()),
// StatefulColorfulTile(key: UniqueKey()),
StatelessColorfulTile(),
StatelessColorfulTile(),
];
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Row(
children: tiles,
),
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.sentiment_very_satisfied),
// child: Icon(Icons.sentiment_very_dissatisfied),
onPressed: swapTiles,
),
);
}
void swapTiles() {
setState(() {
tiles.insert(1, tiles.removeAt(0));
});
}
}
// ignore: must_be_immutable
class StatelessColorfulTile extends StatelessWidget {
Color color = ColorUtil.randomColor();
@override
Widget build(BuildContext context) {
return Container(
color: color,
child: Padding(padding: EdgeInsets.all(70.0))
);
}
}
class StatefulColorfulTile extends StatefulWidget {
StatefulColorfulTile({Key key}) : super(key: key);
@override
State<StatefulWidget> createState() => StatefulColorfulTileState();
}
class StatefulColorfulTileState extends State<StatefulColorfulTile> {
Color color;
@override
void initState() {
super.initState();
color = ColorUtil.randomColor();
}
@override
Widget build(BuildContext context) {
return Container(
color: color,
child: Padding(padding: EdgeInsets.all(70.0))
);
}
}
class ColorUtil {
static Color randomColor() {
var red = Random.secure().nextInt(255);
var greed = Random.secure().nextInt(255);
var blue = Random.secure().nextInt(255);
return Color.fromARGB(255, red, greed, blue);
}
}
複製代碼
上面的代碼效果以下,能夠看到使用StatelessColorfulTile
時,點擊按鈕後兩個色塊能成功交換: ide
接下來咱們把代碼改爲下面這樣煮,重啓:
@override
void initState() {
super.initState();
tiles = [
StatefulColorfulTile(),
StatefulColorfulTile(),
// StatefulColorfulTile(key: UniqueKey()),
// StatefulColorfulTile(key: UniqueKey()),
// StatelessColorfulTile(),
// StatelessColorfulTile(),
];
}
複製代碼
神奇的事情發生了,點擊按鈕後,色塊再也不發生交換:
那在使用StatefulColorfulTile
的前提下,如何讓色塊再次點擊按鈕後能發生交換呢?我猜聰明的你已經想到了,就是設置key屬性,即把代碼改爲下面這個樣子,重啓:
@override
void initState() {
super.initState();
tiles = [
// StatefulColorfulTile(),
// StatefulColorfulTile(),
StatefulColorfulTile(key: UniqueKey()),
StatefulColorfulTile(key: UniqueKey()),
// StatelessColorfulTile(),
// StatelessColorfulTile(),
];
}
複製代碼
效果以下:
接下來就是圖解形成這些效果的緣由了。
StatelessColorfulTile
能交換咱們先來看看StatelessColorfulTile
交換的時候都發生了什麼,先來看看交換前的:
交換後的:
當代碼調用PositionedTiles.setState
交換兩個Widget後,flutter會從上到下逐一對比Widget樹和Element樹中的每一個節點,若是發現節點的runtimeType和key一致的話(這裏沒有key,所以只對比runtimeType),那麼就認爲該Element仍然是有效的,可用複用,因而只須要更改Element的指針,就能夠直接複用。
而因爲StatefulColorfulTile
的顏色信息是存儲在widget中的:
class StatelessColorfulTile extends StatelessWidget {
Color color = ColorUtil.randomColor();
...(略)
}
複製代碼
因此即使色塊Widget由於Widget.canUpdate
返回不須要更新,內部沒有回調到setState
邏輯,也會成功交換。
Element保存了Widget和RenderObject,Widget是負責描述控件樣式,RenderObject則是佈局渲染控制,當Element只更新了Widget,下一次渲染時就會變成新Widget的效果了。
StatefulColorfulTile
要加key才能交換先從代碼的最表面說說StatefulColorfulTile
和StatelessColorfulTile
一個重大的區別,即Color的屬性放的位置不同。
StatelessColorfulTile
的Color屬性是直接放置在Widget下的:
class StatelessColorfulTile extends StatelessWidget {
Color color = ColorUtil.randomColor();
...(略)
}
複製代碼
而StatefulColorfulTile
的Color屬性是放在State下的:
這裏補充一個基礎知識,即State
屬性,最終都會被Element
管理,下面能夠簡單追幾段源碼看看。
首先看看StateFulWidget
的抽象方法:
有了Flutter三棵樹概念之後,咱們應該明白每一個Widget最終都會被建立出對應的Element,而建立的方法正是上面的createElement
,它會調用StatefulElement
構造函數來構造。
接着跟進StatefulElement()
函數,咱們就能清晰地看到StatefulElement
管理了State
,而且拿它來作各類各樣的事了:
明確了State
屬性,最終都會被Element
管理這個大前提後,接下來就好辦了。
咱們先來看看StatefulColorfulTile
不帶key的時候,調用交換函數究竟發生了什麼,依舊是先看交換前的:
交換後的:
相信緣由不用我多說了,首先仍是Widget更新後,flutter會根據runtimeType
和key
比較Widget從而判斷是否須要從新構建Element
,這裏key
爲空,只比較runtimeType
,比較結果必然相等,因此Element
直接複用。
StatefulColorfulTile
在從新渲染時,Color
屬性再也不是從Widget
對象(即自身)裏獲取,而是從Element
的State
裏面獲取,而Element
根本沒發生變化,因此取到的Color
也沒有變化,最終就算怎麼渲染,顏色都是不變的,視覺效果上也就是兩個色塊沒有交換了。
接着看有了key
以後,交換前:
交換後,發現兩邊key
不相等,因而嘗試匹配Element
是否還有相同的id,發現有,因而從新排列Element
讓相同key
的配對:
若是Element這邊沒有key能與新Widget匹配得上,那麼舊的Element會失效,後續根據新Widget從新構建一個Element。
rebuild後,Element已改變,從新渲染後視覺上就看到兩個色塊交換位置了:
熟悉三棵樹原理的咱們知道,Element
就至關於設備上的真實控件,既然Element
的位置變化了,那麼最終屏幕上的控件也就跟着變化了,最終交換後從新渲染給視覺上就是兩個色塊交換了。
好了,本篇博客先到這裏結束了,這裏只是簡單介紹了下Widget中key的做用,但實際上Key還有不少種實現,他們用處各有不一樣,這個由於和本篇目標沒啥太大關係,因此不介紹了,有空本身翻翻官方文檔其實很快也能搞懂了。