開始
在Flutter的每個(gè)Widget中, 都會有key這個(gè)可選屬性. 在剛開始學(xué)習(xí)flutter時(shí), 基本就直接忽略不管了. 對執(zhí)行結(jié)果好像也沒什么影響. 現(xiàn)在來深究下key到底有什么作用.(研究一天時(shí)間, 發(fā)現(xiàn)key
沒什么作用. 快要放棄, 又多寫了幾個(gè)簡單例子, 終于發(fā)現(xiàn)差異~~)
官方文檔介紹
key用于控制控件如何取代樹中的另一個(gè)控件.
如果2個(gè)控件的runtimeType
和key
屬性operator==
, 那么新的控件通過更新底層元素來替換舊的控件(通過調(diào)用Element.update
). 否則舊的控件將從樹上刪除, element
會生成新的控件, 然后新的element
會被插入到樹中.
另外, 使用GlobalKey
做為控件的key
, 允許element
在樹周圍移動(改變父節(jié)點(diǎn)), 而不會丟失狀態(tài). 當(dāng)發(fā)現(xiàn)一個(gè)新的控件時(shí)(它的key
和類型與同一位置上控件不匹配), 但是在前面的結(jié)構(gòu)中有一個(gè)帶有相同key
的小部件, 那么這個(gè)控件將會被移動到新的位置.
GlobalKey
是很昂貴的. 如果不需要使用上述特性, 可以考慮使用Key
, ValueKey
或UniqueKey
替換.
通常, 只有一個(gè)子節(jié)點(diǎn)的widget
不需要指定key
.
實(shí)踐
為了弄清楚這些, 我們有必要了解, Flutter中控件的構(gòu)建流程以及刷新流程. 簡單的做法是在StatelessWidget.build
或者StatefulWidget.build
中下個(gè)斷點(diǎn), 調(diào)試運(yùn)行, 等斷點(diǎn)停下來. 在Debug
視窗的Frames
視圖下可以看到函數(shù)的調(diào)用堆棧. 然后繼續(xù)跑幾步, 可以看到后面的調(diào)用流程.
調(diào)用流程
為了更直觀的查看調(diào)用流程, 省去了MaterialApp
和Scaffold
的包裹.
例子1
void main() {
runApp(Sample1());
}
class Sample1 extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Text('Sample1', textDirection: TextDirection.ltr,);
}
}
從這個(gè)調(diào)用棧我們就可以知道調(diào)用流程. 關(guān)鍵函數(shù)我直接提煉出來.
關(guān)鍵代碼分析
關(guān)鍵代碼
-
Widget.canUpdate
, 用于判斷Widget
是否能復(fù)用, 注意類型相同,key
為空也是可以復(fù)用的.static bool canUpdate(Widget oldWidget, Widget newWidget) { return oldWidget.runtimeType == newWidget.runtimeType && oldWidget.key == newWidget.key; }
Element.updateChild
Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
....
if (newWidget == null) {
if (child != null)
deactivateChild(child);
return null;
}
if (child != null) {
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);
assert(child.widget == newWidget);
assert(() {
child.owner._debugElementWasRebuilt(child);
return true;
}());
return child;
}
deactivateChild(child);
assert(child._parent == null);
}
return inflateWidget(newWidget, newSlot);
}
第一次構(gòu)建時(shí), Element child
參數(shù)為空, 我們需要將StatefulWidget
或StatelessWidget
build出的Widget
傳遞給inflateWidget方法. 后面刷新界面時(shí), Element child
參數(shù)不為空時(shí), 我們可以判斷Element原來持有的widget和build新得出的widget是否相同. 什么情況下會這樣了? 當(dāng)我們緩存了Widget就會這樣.
例子2
void main() {
runApp(Sample2());
}
class _Sample2State extends State<Sample2> {
int count = 0;
Text cacheText;
@override
void initState() {
cacheText = Text(
'cache_text',
textDirection: TextDirection.ltr,
);
}
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
RaisedButton(
child: Text(
'$count',
textDirection: TextDirection.ltr,
),
onPressed: () {
print(this.widget);
setState(() {
count += 1;
});
},
),
cacheText,
Text('no_cache_text', textDirection: TextDirection.ltr),
],
);
}
}
在if (child.widget == newWidget)
這里下個(gè)斷點(diǎn), 當(dāng)點(diǎn)擊按鈕事刷新時(shí), 我們就可以發(fā)現(xiàn)cache_text
會進(jìn)來, 而no_cache_text
不會進(jìn)來. no_cache_text
會進(jìn)入到if (Widget.canUpdate(child.widget, newWidget))
里, 因?yàn)閜arent(Column)沒變, 元素個(gè)數(shù)沒變, 繼續(xù)執(zhí)行update(widget)
@mustCallSuper
void update(covariant Widget newWidget) {
assert(_debugLifecycleState == _ElementLifecycle.active
&& widget != null
&& newWidget != null
&& newWidget != widget
&& depth != null
&& _active
&& Widget.canUpdate(widget, newWidget));
_widget = newWidget;
}
這里其實(shí)就是修改了Element
持有的_widget
指向. 也就是文檔里說的, Widget
被切換, 而Element
會被復(fù)用. 當(dāng)這些都無法滿足時(shí), 就是執(zhí)行inflateWidget
來創(chuàng)建新的Element
. 什么情況下會這樣了, 當(dāng)Widget.canUpdate不為真時(shí), 就是當(dāng)類型或key不同時(shí). 下面舉個(gè)例子:
例子3
class _Sample3State extends State<Sample3> {
int count = 0;
GlobalKey keyOne = GlobalKey();
GlobalKey keyTwo = GlobalKey();
@override
void initState() {}
@override
Widget build(BuildContext context) {
List<Widget> list = [
RaisedButton(
child: Text(
'$count',
textDirection: TextDirection.ltr,
),
onPressed: () {
print(this.widget);
setState(() {
count += 1;
});
},
),
Text(
'key${count%2}',
textDirection: TextDirection.ltr,
key: count % 2 == 0 ? keyOne : keyTwo,
),
];
if (count % 2 == 0) {
list.add(RaisedButton(
onPressed: () {},
child: Text('button text', textDirection: TextDirection.ltr)));
} else {
list.add(Text('just text', textDirection: TextDirection.ltr));
}
return Column(
children: list,
);
}
}
在Element.inflateWidget
下個(gè)斷點(diǎn), 每次點(diǎn)擊按鈕, 都會調(diào)用inflateWidget
生Column
的后面2個(gè)Widget
. 如果去掉第二個(gè)控件的key
屬性, 則不會每次都執(zhí)行Element.inflateWidget
. 也就是說一般情況下, 我們無需指定key
屬性, Element
就能復(fù)用.
文檔里還指出使用了GlobalKey
, 一個(gè)Element
可以從樹的一個(gè)位置復(fù)用到樹的其它位置. 其相關(guān)核心代碼在inflateWidget
和_retakeInactiveElement
方法里.
Element.inflateWidget
Element inflateWidget(Widget newWidget, dynamic newSlot) {
assert(newWidget != null);
final Key key = newWidget.key;
if (key is GlobalKey) {
final Element newChild = _retakeInactiveElement(key, newWidget);
if (newChild != null) {
assert(newChild._parent == null);
assert(() { _debugCheckForCycles(newChild); return true; }());
newChild._activateWithParent(this, newSlot);
final Element updatedChild = updateChild(newChild, newWidget, newSlot);
assert(newChild == updatedChild);
return updatedChild;
}
}
final Element newChild = newWidget.createElement();
assert(() { _debugCheckForCycles(newChild); return true; }());
newChild.mount(this, newSlot);
assert(newChild._debugLifecycleState == _ElementLifecycle.active);
return newChild;
}
Element _retakeInactiveElement(GlobalKey key, Widget newWidget) {
final Element element = key._currentElement;
if (element == null)
return null;
if (!Widget.canUpdate(element.widget, newWidget))
return null;
assert(() {
if (debugPrintGlobalKeyedWidgetLifecycle)
debugPrint('Attempting to take $element from ${element._parent ?? "inactive elements list"} to put in $this.');
return true;
}());
final Element parent = element._parent;
if (parent != null) {
assert(() {
if (parent == this) {
throw new FlutterError(
...
);
}
parent.owner._debugTrackElementThatWillNeedToBeRebuiltDueToGlobalKeyShenanigans(
parent,
key,
);
return true;
}());
parent.forgetChild(element);
parent.deactivateChild(element);
}
assert(element._parent == null);
owner._inactiveElements.remove(element);
return element;
}
代碼大概意思是, 首先如果widget
的key
值不為空并且為GlobalKey
類型時(shí), 會判斷key._currentElement
值所指向的widget
, 和當(dāng)前widget
的類型key
都相同. 那么就從舊的父節(jié)點(diǎn)上移除. 作為當(dāng)前的節(jié)點(diǎn)的子widget
之一. 否則將進(jìn)行真實(shí)的創(chuàng)建新的Element
. 下面舉個(gè)例子
例子4
void main() {
runApp(Sample4());
}
class Sample4 extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return _Sample4State();
}
}
class _Sample4State extends State<Sample4> {
int count = 0;
GlobalKey key = GlobalKey();
@override
Widget build(BuildContext context) {
var child;
if (count % 2 == 0) {
child = Padding(
padding: EdgeInsets.all(10.0),
child: Text(
'text 2',
textDirection: TextDirection.ltr,
key: key,
),
);
} else {
child = Container(
child: Text(
'text 2',
textDirection: TextDirection.ltr,
key: key,
),
);
}
return Column(
children: <Widget>[
RaisedButton(
child: Text(
'$count',
textDirection: TextDirection.ltr,
),
onPressed: () {
print(this.widget);
setState(() {
count += 1;
});
},
),
child
],
);
}
}
在Element._retakeInactiveElement
打個(gè)斷點(diǎn), 會發(fā)現(xiàn)Padding
里的text 1
與Container
里的text 2
復(fù)用了.
GlobalKey
abstract class GlobalKey<T extends State<StatefulWidget>> extends Key {
static final Map<GlobalKey, Element> _registry = <GlobalKey, Element>{};
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;
}
...
void GlobalKey._register(Element element) {
assert(() {
if (_registry.containsKey(this)) {
assert(element.widget != null);
assert(_registry[this].widget != null);
assert(element.widget.runtimeType != _registry[this].widget.runtimeType);
_debugIllFatedElements.add(_registry[this]);
}
return true;
}());
_registry[this] = element;
}
....
}
void Element.mount(Element parent, dynamic newSlot) {
...
if (widget.key is GlobalKey) {
final GlobalKey key = widget.key;
key._register(this);
}
...
}
當(dāng)過閱讀代碼我們可以知道GlobalKey
在Element
被創(chuàng)建時(shí)就寫入到一個(gè)靜態(tài)Map
里, 并且關(guān)聯(lián)了當(dāng)前的Element
對象. 所以通過GlobalKey
可以查詢當(dāng)前控件相關(guān)的信息. 下面舉個(gè)例子
例子5
void main() {
runApp(Sample5());
}
class Sample5 extends StatefulWidget {
@override
State createState() {
return _Sample5State();
}
}
class _Sample5State extends State<Sample5> {
final key = GlobalKey<Sample5WidgetState>();
@override
void initState() {
//calling the getHeight Function after the Layout is Rendered
WidgetsBinding.instance.addPostFrameCallback((_) => getHeight());
super.initState();
}
void getHeight() {
final Sample5WidgetState state = key.currentState;
final BuildContext context = key.currentContext;
final RenderBox box = state.context.findRenderObject();
print(state.number);
print(box.size.height);
print(context.size.height);
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Sample5Widget(
key: key,
),
),
);
}
}
class Sample5Widget extends StatefulWidget {
Sample5Widget({Key key}) : super(key: key);
@override
State<StatefulWidget> createState() => Sample5WidgetState();
}
class Sample5WidgetState extends State<Sample5Widget> {
int number = 12;
@override
Widget build(BuildContext context) {
return new Container(
child: new Text(
'text',
style: const TextStyle(fontSize: 32.0, fontWeight: FontWeight.bold),
),
);
}
}
通過將GlobalKey
傳遞給下層, 我們在上層通過GlobalKey
能夠獲取到Sample5WidgetState
對象.
除了上面所述, 那么什么時(shí)候我們需要使用key
? 官方有個(gè)例子: Implement Swipe to Dismiss, 為每個(gè)item指定了key
屬性'Key'(不是GlobaKey
). 也就是對于列表, 為了區(qū)分不同的子項(xiàng), 也可能用到key
. 一般key
值與當(dāng)前item的數(shù)據(jù)關(guān)聯(lián). 刷新時(shí), 同一個(gè)數(shù)據(jù)指向的item復(fù)用, 不同的則無法復(fù)用.
總結(jié)
- 對于列表可以使用
key
唯一關(guān)聯(lián)數(shù)據(jù). -
GlobaKey
可以讓不同的頁面復(fù)用視圖. 參見例子4 -
GlobaKey
可以查詢節(jié)點(diǎn)相關(guān)信息. 參見例子5
執(zhí)行流程先復(fù)用widget
, 不行就創(chuàng)建widget
復(fù)用Element
, 再不行就都重新創(chuàng)建.