Key
在Flutter的源碼中可以說是無處不在,但是我們?nèi)粘V写_不怎么使用它。有點(diǎn)像是“最熟悉的陌生人”,那么今天就來說說這個“陌生人”,揭開它神秘的面紗。
概念
Key
是Widget
、Element
和SemanticsNode
的標(biāo)識符。 只有當(dāng)新的Widget
的Key
與當(dāng)前Element
中Widget
的Key
相同時,它才會被用來更新現(xiàn)有的Element
。Key
在具有相同父級的Element
之間必須是唯一的。
以上定義是源碼中關(guān)于Key
的解釋。通俗的說就是Widget
的標(biāo)識,幫助實(shí)現(xiàn)Element
的復(fù)用。關(guān)于它的說明源碼中也提供了YouTube的視頻鏈接:When to Use Keys。如果你無法訪問,可以看Google 官方在優(yōu)酷上傳的。
例子
視頻中的例子很簡單且具有代表性,所以本文將采用它來介紹今天的內(nèi)容。
首先上代碼:
import 'dart:math';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
List<Widget> widgets;
@override
void initState() {
super.initState();
widgets = [
StatelessColorfulTile(),
StatelessColorfulTile()
];
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Row(
children: widgets,
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.refresh),
onPressed: _swapTile,
),
);
}
_swapTile() {
setState(() {
widgets.insert(1, widgets.removeAt(0));
});
}
}
class StatelessColorfulTile extends StatelessWidget {
final Color _color = Utils.randomColor();
@override
Widget build(BuildContext context) {
return Container(
height: 150,
width: 150,
color: _color,
);
}
}
class Utils {
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);
}
}
代碼可以直接復(fù)制到DartPad中運(yùn)行查看效果。 或者點(diǎn)擊這里直接運(yùn)行。
效果很簡單,就是兩個彩色方塊,點(diǎn)擊右下角的按鈕后交換兩個方塊的位置。這里我就不放具體的效果圖了。實(shí)際效果也和我們預(yù)期的一樣,兩個方塊成功交換位置。
發(fā)現(xiàn)問題
上面的方塊是StatelessWidget
,那我們把它換成StatefulWidget
呢?。
class StatefulColorfulTile extends StatefulWidget {
StatefulColorfulTile({Key key}) : super(key: key);
@override
StatefulColorfulTileState createState() => StatefulColorfulTileState();
}
class StatefulColorfulTileState extends State<StatefulColorfulTile> {
final Color _color = Utils.randomColor();
@override
Widget build(BuildContext context) {
return Container(
height: 150,
width: 150,
color: _color,
);
}
}
再次執(zhí)行代碼,發(fā)現(xiàn)方塊沒有“交換”。這是為什么?
分析問題
首先要知道Flutter中有三棵樹,分別是==Widget Tree==、==Element Tree== 和 ==RenderObject Tree==。
- Widget:
Element
的配置信息。與Element
的關(guān)系可以是一對多,一份配置可以創(chuàng)造多個Element
實(shí)例。 - Element:
Widget
的實(shí)例化,內(nèi)部持有Widget
和RenderObject
。 - RenderObject:負(fù)責(zé)渲染繪制。
簡單的比擬一下,Widget
有點(diǎn)像是產(chǎn)品經(jīng)理,規(guī)劃產(chǎn)品整理需求。Element
則是UI小姐姐,根據(jù)原型整理出最終設(shè)計(jì)圖。RenderObject
就是我們程序員,負(fù)責(zé)具體的落地實(shí)現(xiàn)。
代碼中可以確定一點(diǎn),兩個方塊的Widget肯定是交換了。既然Widget
沒有問題,那就看看Element
。
但是為什么StatelessWidget
可以成功,換成StatefulWidget
就失效了?
點(diǎn)擊按鈕調(diào)用setState
方法,依次執(zhí)行:
graph TB
A["_element.markNeedsBuild()"] -- 標(biāo)記自身元素dirty為true --> B["owner.scheduleBuildFor()"]
B --添加至_dirtyElements--> D["drawFrame()"]
D --> E["buildScope()"]
E --> F["_dirtyElements[index].rebuild()"]
F --> G["performRebuild()"]
G --> H["updateChild()"]
我們重點(diǎn)看一下Element
的updateChild
方法:
@protected
Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
// 如果'newWidget'為null,而'child'不為null,那么我們刪除'child',返回null。
if (newWidget == null) {
if (child != null)
deactivateChild(child);
return null;
}
if (child != null) {
// 兩個widget相同,位置不同更新位置,返回child。這里比較的是hashCode
if (child.widget == newWidget) {
if (child.slot != newSlot)
updateSlotForChild(child, newSlot);
return child;
}
// 我們的交換例子處理在這里
if (Widget.canUpdate(child.widget, newWidget)) {
if (child.slot != newSlot)
updateSlotForChild(child, newSlot);
child.update(newWidget);
return child;
}
deactivateChild(child);
}
// 如果無法更新復(fù)用,那么創(chuàng)建一個新的Element并返回。
return inflateWidget(newWidget, newSlot);
}
Widget
的canUpdate
方法:
static bool canUpdate(Widget oldWidget, Widget newWidget) {
return oldWidget.runtimeType == newWidget.runtimeType
&& oldWidget.key == newWidget.key;
}
這里出現(xiàn)了我們今天的主角Key
,不過我們先放在一邊。canUpdate
方法的作用是判斷newWidget是否可以替代oldWidget作為Element
的配置。 一開始也提到了,Element
會持有Widget。
該方法判斷的依據(jù)就是runtimeType
和key
是否相等。在我們上面的例子中,不管是StatelessWidget
還是StatefulWidget
的方塊,顯然canUpdate
都會返回true。因此執(zhí)行child.update(newWidget)
方法,就是將持有的Widget更新了。
不知道這里大家有沒有注意到,這里并沒有更新state
。我們看一下StatefulWidget
源碼:
abstract class StatefulWidget extends Widget {
const StatefulWidget({ Key key }) : super(key: key);
@override
StatefulElement createElement() => StatefulElement(this);
@protected
State createState();
}
StatefulWidget
中創(chuàng)建的是StatefulElement
,它是Element
的子類。
class StatefulElement extends ComponentElement {
StatefulElement(StatefulWidget widget)
: _state = widget.createState(),
super(widget) {
_state._element = this;
_state._widget = widget;
}
@override
Widget build() => state.build(this);
State<StatefulWidget> get state => _state;
State<StatefulWidget> _state;
...
}
通過調(diào)用StatefulWidget
的createElement
方法,最終執(zhí)行createState
創(chuàng)建出state并持有。也就是說StatefulElement
才持有state。
所以我們上面兩個StatefulWidget
的方塊的交換,實(shí)際只是交換了“身體”,而“靈魂”沒有交換。所以不管你怎么點(diǎn)擊按鈕都是沒有變化的。
解決問題
找到了原因,那么怎么解決它?那就是設(shè)置一個不同的Key
:
@override
void initState() {
super.initState();
widgets = [
StatefulColorfulTile(key: const Key("1")),
StatefulColorfulTile(key: const Key("2"))
];
}
但是這里要注意的是,這里不是說添加key以后,在canUpdate
方法返回false,最后執(zhí)行inflateWidget(newWidget, newSlot)
方法創(chuàng)建新的Element
。(很多相關(guān)文章對于此處的說明都有誤區(qū)。。。好吧我承認(rèn)我一開始也被誤導(dǎo)了。。。)
@protected
Element inflateWidget(Widget newWidget, dynamic newSlot) {
final Key key = newWidget.key;
if (key is GlobalKey) {
final Element newChild = _retakeInactiveElement(key, newWidget);
if (newChild != null) {
newChild._activateWithParent(this, newSlot);
final Element updatedChild = updateChild(newChild, newWidget, newSlot);
assert(newChild == updatedChild);
return updatedChild;
}
}
// 這里就調(diào)用到了createElement,重新創(chuàng)建了Element
final Element newChild = newWidget.createElement();
newChild.mount(this, newSlot);
return newChild;
}
如果如此,那么執(zhí)行createElement
方法勢必會重新創(chuàng)建state,那么方塊的顏色也就隨機(jī)變了。當(dāng)然此種情況并不是不存在,比如我們給現(xiàn)有的方塊外包一層Padding
(SingleChildRenderObjectElement
):
@override
void initState() {
super.initState();
widgets = [
Padding(
padding: const EdgeInsets.all(8.0),
child: StatefulColorfulTile(key: Key("1"),)
),
Padding(
padding: const EdgeInsets.all(8.0),
child: StatefulColorfulTile(key: Key("2"),)
),
];
}
這種情況下,交換后比較外層Padding
不變,接著比較內(nèi)層StatefulColorfulTile
,因?yàn)閗ey不相同導(dǎo)致顏色隨機(jī)改變。因?yàn)閮蓚€方塊位于不同子樹,兩者在逐層對比中用到的就是canUpdate
方法返回false來更改。
而本例是方塊的外層是Row
(MultiChildRenderObjectElement
),是對比兩個List,存在不同。關(guān)鍵在于update
時調(diào)用的RenderObjectElement.updateChildren
方法。
@protected
List<Element> updateChildren(List<Element> oldChildren, List<Widget> newWidgets, { Set<Element> forgottenChildren }) {
...
int newChildrenTop = 0;
int oldChildrenTop = 0;
int newChildrenBottom = newWidgets.length - 1;
int oldChildrenBottom = oldChildren.length - 1;
final List<Element> newChildren = oldChildren.length == newWidgets.length ?
oldChildren : List<Element>(newWidgets.length);
Element previousChild;
// 從前往后依次對比,相同的更新Element,記錄位置,直到不相等時跳出循環(huán)。
while ((oldChildrenTop <= oldChildrenBottom) &&
(newChildrenTop <= newChildrenBottom)) {
final Element oldChild = replaceWithNullIfForgotten(oldChildren[oldChildrenTop]);
final Widget newWidget = newWidgets[newChildrenTop];
// 注意這里的canUpdate,本例中在沒有添加key時返回true。
// 因此直接執(zhí)行updateChild,本循環(huán)結(jié)束返回newChildren。后面因條件不滿足都在不執(zhí)行。
// 一旦添加key,這里返回false,不同之處就此開始。
if (oldChild == null || !Widget.canUpdate(oldChild.widget, newWidget))
break;
final Element newChild = updateChild(oldChild, newWidget, previousChild);
newChildren[newChildrenTop] = newChild;
previousChild = newChild;
newChildrenTop += 1;
oldChildrenTop += 1;
}
// 從后往前依次對比,記錄位置,直到不相等時跳出循環(huán)。
while ((oldChildrenTop <= oldChildrenBottom) &&
(newChildrenTop <= newChildrenBottom)) {
final Element oldChild = replaceWithNullIfForgotten(oldChildren[oldChildrenBottom]);
final Widget newWidget = newWidgets[newChildrenBottom];
if (oldChild == null || !Widget.canUpdate(oldChild.widget, newWidget))
break;
oldChildrenBottom -= 1;
newChildrenBottom -= 1;
}
// 至此,就可以得到新舊List中不同Weiget的范圍。
final bool haveOldChildren = oldChildrenTop <= oldChildrenBottom;
Map<Key, Element> oldKeyedChildren;
// 如果存在中間范圍,掃描舊children,獲取所有的key與Element保存至oldKeyedChildren。
if (haveOldChildren) {
oldKeyedChildren = <Key, Element>{};
while (oldChildrenTop <= oldChildrenBottom) {
final Element oldChild = replaceWithNullIfForgotten(oldChildren[oldChildrenTop]);
if (oldChild != null) {
if (oldChild.widget.key != null)
oldKeyedChildren[oldChild.widget.key] = oldChild;
else
// 沒有key就移除對應(yīng)的Element
deactivateChild(oldChild);
}
oldChildrenTop += 1;
}
}
// 更新中間不同的部分
while (newChildrenTop <= newChildrenBottom) {
Element oldChild;
final Widget newWidget = newWidgets[newChildrenTop];
if (haveOldChildren) {
final Key key = newWidget.key;
if (key != null) {
// key不為null,通過key獲取對應(yīng)的舊Element
oldChild = oldKeyedChildren[key];
if (oldChild != null) {
if (Widget.canUpdate(oldChild.widget, newWidget)) {
oldKeyedChildren.remove(key);
} else {
oldChild = null;
}
}
}
}
// 本例中這里的oldChild.widget與newWidget hashCode相同,在updateChild中成功被復(fù)用。
final Element newChild = updateChild(oldChild, newWidget, previousChild);
newChildren[newChildrenTop] = newChild;
previousChild = newChild;
newChildrenTop += 1;
}
// 重置
newChildrenBottom = newWidgets.length - 1;
oldChildrenBottom = oldChildren.length - 1;
// 將后面相同的Element更新后添加到newChildren,至此形成新的完整的children。
while ((oldChildrenTop <= oldChildrenBottom) &&
(newChildrenTop <= newChildrenBottom)) {
final Element oldChild = oldChildren[oldChildrenTop];
final Widget newWidget = newWidgets[newChildrenTop];
final Element newChild = updateChild(oldChild, newWidget, previousChild);
newChildren[newChildrenTop] = newChild;
previousChild = newChild;
newChildrenTop += 1;
oldChildrenTop += 1;
}
// 清除舊列表中多余的Element
if (haveOldChildren && oldKeyedChildren.isNotEmpty) {
for (Element oldChild in oldKeyedChildren.values) {
if (forgottenChildren == null || !forgottenChildren.contains(oldChild))
deactivateChild(oldChild);
}
}
return newChildren;
}
這個方法有點(diǎn)復(fù)雜,詳細(xì)的執(zhí)行流程我在代碼中添加了注釋。看完這個diff算法,只能說一句:妙啊!!
到此也就解釋了我們一開始提出的問題。不知道你對這不起眼的key
是不是有了更深的認(rèn)識。通過上面的例子可以總結(jié)以下三點(diǎn):
一般情況下不設(shè)置key也會默認(rèn)復(fù)用
Element
。對于更改同一父級下Widget(尤其是
runtimeType
不同的Widget)的順序或是增刪,使用key
可以更好的復(fù)用Element
,提升性能。StatefulWidget
使用key,可以在發(fā)生變化時保持state。不至于發(fā)生本例中“身體交換”的bug。
Key的種類
上面例子中我們用到了Key
,其實(shí)它還有許多種類。
1.LocalKey
LocalKey
繼承自 Key
,在同一父級的Element
之間必須是唯一的。(當(dāng)然了,你要是寫成不唯一也行,不過后果自負(fù)哈。。。)
我們基本不直接使用LocalKey
,而是使用的它的子類:
ValueKey
我們上面使用到的Key
,其實(shí)就是ValueKey<String>
。它主要是使用特定類型的值來做標(biāo)識的,像是“值引用”,比如int、String等類型。我們看它源碼中的 ==
操作符方法:
class ValueKey<T> extends LocalKey {
const ValueKey(this.value);
final T value;
@override
bool operator ==(dynamic other) {
if (other.runtimeType != runtimeType)
return false;
final ValueKey<T> typedOther = other;
return value == typedOther.value; // <---
}
...
}
ObjectKey
有“值引用”,就有“對象引用”。主要還是==
操作符方法:
class ObjectKey extends LocalKey {
const ObjectKey(this.value);
final Object value;
@override
bool operator ==(dynamic other) {
if (other.runtimeType != runtimeType)
return false;
final ObjectKey typedOther = other;
return identical(value, typedOther.value); // <---
}
...
}
UniqueKey
會生成一個獨(dú)一無二的key值。
class UniqueKey extends LocalKey {
UniqueKey();
@override
String toString() => '[#${shortHash(this)}]';
}
String shortHash(Object object) {
return object.hashCode.toUnsigned(20).toRadixString(16).padLeft(5, '0');
}
PageStorageKey
用于保存和還原比Widget生命周期更長的值。比如用于保存滾動的偏移量。每次滾動完成時,PageStorage
會保存其滾動偏移量。 這樣在重新創(chuàng)建Widget時可以恢復(fù)之前的滾動位置。類似的,在ExpansionTile
中用于保存展開與閉合的狀態(tài)。
具體的實(shí)現(xiàn)原理也很簡單,看看PageStorage
的源碼就清楚了,這里就不展開了。
2.GlobalKey
介紹
GlobalKey
也繼承自 Key
,在整個應(yīng)用程序中必須是唯一的。GlobalKey
源碼有點(diǎn)長,我就不全部貼過來了。
@optionalTypeArgs
abstract class GlobalKey<T extends State<StatefulWidget>> extends Key {
factory GlobalKey({ String debugLabel }) => LabeledGlobalKey<T>(debugLabel);
const GlobalKey.constructor() : super.empty();
static final Map<GlobalKey, Element> _registry = <GlobalKey, Element>{};
// 在`Element的 `mount`中注冊GlobalKey。
void _register(Element element) {
_registry[this] = element;
}
// 在`Element的 `unmount`中注銷GlobalKey。
void _unregister(Element element) {
if (_registry[this] == element)
_registry.remove(this);
}
Element get _currentElement => _registry[this];
BuildContext get currentContext => _currentElement;
Widget get currentWidget => _currentElement?.widget;
T get currentState {
final Element element = _currentElement;
if (element is StatefulElement) {
final StatefulElement statefulElement = element;
final State state = statefulElement.state;
if (state is T)
return state;
}
return null;
}
...
}
它的內(nèi)部存在一個Map<GlobalKey, Element>
的靜態(tài)Map,通過調(diào)用_register
、_unregister
方法來添加和刪除Element
。同時它的內(nèi)部還持有當(dāng)前的Element
、Widget
甚至State
。可以看到 GlobalKey
是非常昂貴的,沒有特別的復(fù)用需求,不建議使用它。
怎么復(fù)用呢?GlobalKey
在上面inflateWidget
的源碼中出現(xiàn)過一次。當(dāng)發(fā)現(xiàn)key是GlobalKey
時,使用_retakeInactiveElement
方法復(fù)用Element
。
Element _retakeInactiveElement(GlobalKey key, Widget newWidget) {
final Element element = key._currentElement;
if (element == null)
return null;
if (!Widget.canUpdate(element.widget, newWidget))
return null;
final Element parent = element._parent;
if (parent != null) {
parent.forgetChild(element);
parent.deactivateChild(element);
}
owner._inactiveElements.remove(element);
return element;
}
如果獲取到了Element
,那么就從舊的節(jié)點(diǎn)上移除并返回。否則將在inflateWidget
重新創(chuàng)建新的Element
。
使用
首先就是上面提到的使用相同的
GlobalKey
來實(shí)現(xiàn)復(fù)用。利用
GlobalKey
持有的BuildContext
。比如常見的使用就是獲取Widget的寬高信息,通過BuildContext
可以在其中獲取RenderObject
或Size
,從而拿到寬高信息。這里就不貼代碼了,有需要可以看此處示例。利用
GlobalKey
持有的State
,實(shí)現(xiàn)在外部調(diào)用StatefulWidget
內(nèi)部方法。比如常用GlobalKey<NavigatorState>
來實(shí)現(xiàn)無Context跳轉(zhuǎn)頁面,在點(diǎn)擊推送信息跳轉(zhuǎn)指定頁面就需要用到。
先創(chuàng)建一個GlobalKey<NavigatorState>
:
static GlobalKey<NavigatorState> navigatorKey = new GlobalKey();
添加至MaterialApp:
MaterialApp(
navigatorKey: navigatorKey,
...
);
然后就是調(diào)用push方法:
navigatorKey.currentState.push(MaterialPageRoute(
builder: (BuildContext context) => MyPage(),
));
通過GlobalKey
持有的State
,就可以調(diào)用其中的方法、獲取數(shù)據(jù)。
LabeledGlobalKey
它是一個帶有標(biāo)簽的GlobalKey
。 該標(biāo)簽僅用于調(diào)試,不用于比較。
GlobalObjectKey
同上ObjectKey
。區(qū)別在于它是GlobalKey
。
思考題
最后來個思考題:對于可選參數(shù)key,我搜索了一下Flutter的源碼。發(fā)現(xiàn)只有Dismissible
這個滑動刪除組件要求必須傳入key。結(jié)合今天的內(nèi)容,想想是為什么?如果傳入相同的key,會發(fā)生什么?
本篇是“說說”系列第三篇,前兩篇鏈接奉上:
PS:此系列都是自己的學(xué)習(xí)記錄與總結(jié),盡力做到“通俗易懂”和“看著一篇就夠了”。不過也不現(xiàn)實(shí),學(xué)習(xí)之路沒有捷徑。
寫著寫著,就寫的有點(diǎn)多了。本想著拆成兩篇,想想算了。畢竟我是一名月更選手,哈哈~~
如果本文對你有所幫助或啟發(fā)的話,還請不吝點(diǎn)贊收藏支持一波。同時也多多支持我的Flutter開源項(xiàng)目flutter_deer。
我們下個月見~~