1.前言
本文涵蓋了Widget经伙,State,BuildContext勿锅,InheritedWidget等術(shù)語的相關(guān)概念帕膜,并著力解答以下幾個(gè)問題:
- StatefulWidget與StatelessWidget的區(qū)別;
- 什么是BuildContext溢十;
- 什么是State垮刹,我們?cè)撊绾芜\(yùn)用;
- BuildContext與State之間的關(guān)系张弛;
- InheritedWidget以及Widget樹間的信息傳遞荒典;
- rebuild概念。
2.Widget
在Flutter中吞鸭,幾乎任何事物都是Widget寺董。
可以把Widget想象成一種可視化組件,或者應(yīng)用中可以與可視化組件進(jìn)行交互的模塊刻剥。
Widget樹
Flutter中所有的WIdget都以樹狀結(jié)構(gòu)呈現(xiàn)遮咖。
一個(gè)包含其他Widget的Widget被稱為父Widget或者Widget容器,被包含在父Widget下的Widget被稱為子Widget造虏。
下面我們來分析示例代碼中的Widget樹結(jié)構(gòu):
其樹狀結(jié)構(gòu)如下:
BuildContext
BuildContext是Widget樹結(jié)構(gòu)中每個(gè)Widget的上下文環(huán)境御吞,每個(gè)BuildContext都只屬于一個(gè)Widget踢械。
如果Widget A有多個(gè)子Widget,則Widget A的BuildContext是其子Widget的BuildContext的父context魄藕。
為了便于理解每個(gè)context的作用范圍,我們將前文中的Widget樹狀圖中的BuildContext用顏色進(jìn)行標(biāo)注撵术,效果如下:
從BuildContext的繼承關(guān)系中背率,我們可以很容易找到Widget的父級(jí)(祖先)Widget。例如嫩与,在上圖Scaffold > Center > Column > Text這一結(jié)構(gòu)中寝姿,
通過代碼context.ancestorWidgetOfExactType(Scaffold)
就可以獲取到當(dāng)前context下的第一個(gè)Scaffold。
同理划滋,也可以通過父context的關(guān)系找到對(duì)應(yīng)的子Widget饵筑,但是并不推薦這么使用,我們將在后文解釋原因处坪。
StatelessWidget
StatelessWidget一旦創(chuàng)建就無法進(jìn)行修改根资,這意味著它不會(huì)因?yàn)橥獠織l件變化而重新繪制。
一個(gè)典型的StatelessWidget示例如下:
如代碼所見同窘,StatelessWidget的生命周期非常簡單明了:
- 初始化玄帕;
- 通過
build()
方法進(jìn)行渲染。
StatefulWidget
與StatelessWidget相對(duì)應(yīng)的另一種Widget想邦,它可以在其生命周期中操作內(nèi)部持有數(shù)據(jù)的變化裤纹,這些數(shù)據(jù)被稱為State,這樣的Widget也叫做StatefulWidget丧没。
典型的StatefulWidget有Checkbox鹰椒,Radio,Switch等相關(guān)組件呕童,其State發(fā)生的變化將直接體現(xiàn)到UI上進(jìn)行更新漆际。
簡單的StatefulWidget示例:
我們將會(huì)在State部分詳細(xì)講解StatefulWidget的結(jié)構(gòu)與生命周期。
3.State
State作為StatefulWidget內(nèi)部數(shù)據(jù)拉庵,它的作用主要在于兩點(diǎn):
- 定義Widget的交互行為灿椅;
- 調(diào)整Widget的布局顯示。
任何State一旦發(fā)生調(diào)整都會(huì)使StatefulWidget進(jìn)行rebuild操作钞支。
BuildContext與State關(guān)系
在StatefulWidget中茫蛹,State與BuildContext唯一相關(guān),State不能修改所屬的BuildContext烁挟,而且當(dāng)Widget在樹結(jié)構(gòu)中發(fā)生位置變化時(shí)(該操作也會(huì)導(dǎo)致BuildContext的變化)婴洼,這樣的關(guān)系依然保持。
可以這樣認(rèn)為撼嗓,一旦State與BuildContext建立了關(guān)聯(lián)柬采,這種關(guān)系將一直固定存在欢唾,意味著我們不能直接通過其他BuildContext獲取到當(dāng)前context下的state。
StatefulWidget生命周期
正如前文示例代碼所示粉捻,State作為StatefulWidget的主體昧诱,它可以在多個(gè)節(jié)點(diǎn)(@override所標(biāo)記的重寫方法)對(duì)State進(jìn)行調(diào)整蝌以。
當(dāng)然,還有didUpdateWidget()
,deactivate()
畅涂,reassemble()
等重寫方法并不在本文范疇中萝勤。
在下面的時(shí)序圖中我們將完整地了解StatefulWidget各個(gè)方法的調(diào)用順序(已省略部分方法)楚里,以及State與BuildContext的關(guān)聯(lián)時(shí)機(jī)旬牲,State的生效時(shí)機(jī)等:
initState()
initState()
是構(gòu)造方法執(zhí)行之后第一個(gè)調(diào)用的方法,它的執(zhí)行完成標(biāo)志著state對(duì)象初始化完畢呢燥,并且在生命周期中只被調(diào)用一次崭添。
該方法重寫主要完成一些額外的初始化工作,例如animation和controller的相關(guān)初始化等叛氨,重寫時(shí)需要調(diào)用super.initState()
來完成父類的初始化呼渣。
在該方法中,context對(duì)象可以訪問但是并不能拿來使用力试,因?yàn)榇藭r(shí)state與context并沒有建立關(guān)聯(lián)徙邻。
didChangeDependencies()
didChangeDependencies()
是第二個(gè)調(diào)用的方法,在這一步中context可以直接使用畸裳。
該方法一般在Widget自身和InheritedWidget相關(guān)聯(lián)時(shí)或者需要?jiǎng)?chuàng)建基于context的監(jiān)聽時(shí)需要重寫缰犁,且重寫時(shí)需要調(diào)用super.didChangeDependencies()
。
注意怖糊,如果Widget和InheritedWidget進(jìn)行了關(guān)聯(lián)帅容,則Widget每一次進(jìn)行rebuild操作時(shí)該方法都會(huì)重復(fù)調(diào)用。
build()
build()
方法在didChangeDependencies()
和didUpdateWidget()
之后執(zhí)行伍伤,是構(gòu)建Widget及其樹形結(jié)構(gòu)的位置并徘。
該方法會(huì)在state對(duì)象發(fā)生改變或者InheritedWidget向其注冊(cè)的Widget發(fā)起通知時(shí)進(jìn)行調(diào)用,我們可以在setState((){...})
方法的閉包中強(qiáng)制Widget進(jìn)行rebuild(重繪)操作扰魂。
dispose()
dispose()
方法會(huì)在Widget銷毀時(shí)調(diào)用麦乞。該方法進(jìn)行一些清理操作(例如,listener劝评,controller等)姐直,注意在方法最后調(diào)用super.dispose()
。
如何選擇StatefulWidget與StatelessWidget
回答這個(gè)問題之前蒋畜,我們先不妨問問自己:在Widget是生命周期中声畏,是否需要一個(gè)變量來改變Widget,并且考慮如何對(duì)Widget進(jìn)行rebuild操作姻成。
如果我們回答yes插龄,那就需要StatefulWidget愿棋,否則,就應(yīng)該選擇StatelessWidget均牢。
舉個(gè)兩個(gè)栗子:
- 試想需要?jiǎng)?chuàng)建一個(gè)包含CheckBox的列表糠雨,列表中的每一項(xiàng)都包含了標(biāo)題和CheckBox的狀態(tài)。當(dāng)點(diǎn)擊列表中的每一項(xiàng)時(shí)徘跪,CheckBox的狀態(tài)也隨之切換见秤。在這種場景下,需要使用StatefulWidget來記錄每一項(xiàng)的狀態(tài)真椿,以及通過它才能對(duì)CheckBox進(jìn)行重繪。
- 在界面中有一個(gè)Form表單乎澄,表單允許用戶輸入數(shù)據(jù)并發(fā)送到服務(wù)器突硝。如果不需要在提交前做一些數(shù)據(jù)驗(yàn)證或者其他操作,StatelessWidget足夠可用置济。
StatefulWidget結(jié)構(gòu)
正如前文代碼段所示解恰,StatefulWidget包含兩個(gè)部分:
- 定義Widget部分;
- 定義State部分浙于。
定義Widget部分
該部分屬于StatefulWidget的public部分(文件外部可以通過import
訪問到)护盈,在這里可以對(duì)Widget做一些初始化自定義,以及通過重寫createState()
方法與私有的State對(duì)象進(jìn)行關(guān)聯(lián)羞酗。
注意腐宋,任何需要調(diào)整的變量都不應(yīng)該定義在這里,因?yàn)樵谡麄€(gè)Widget生命周期中這里的變量都不會(huì)被改變檀轨,示例代碼中
parameter
前的final
關(guān)鍵字也說明了這點(diǎn)胸竞。
定義State部分
該部分屬于StatefulWidget的private部分(dart語言中以下劃線開頭聲明的類名,方法名参萄,變量名等都屬于作用域范圍下的私有聲明)卫枝,這里定義的變量在Widget生命周期中可以調(diào)整,并且這些調(diào)整可以應(yīng)用到Widget的重繪上讹挎。
同時(shí)校赤,_MyStatefulWidgetState
內(nèi)部可以通過widget.{變量名}
訪問到與之關(guān)聯(lián)的Widget中的變量,例如widget.parameter筒溃。
Widget唯一標(biāo)識(shí) - Key
在Flutter中马篮,每個(gè)Widget都有唯一標(biāo)識(shí)(Key),該標(biāo)識(shí)由系統(tǒng)框架創(chuàng)建铡羡,并且傳遞給Widget構(gòu)造方法中的可選參數(shù)*key*
积蔚。
如果不顯式地傳入key,系統(tǒng)會(huì)自動(dòng)創(chuàng)建一個(gè)烦周,在一些特殊情況下尽爆,必須傳入key值怎顾,例如需要通過key來直接獲取對(duì)應(yīng)Widget的時(shí)候。
Flutter框架提供了下列key方案:
GlobalKey
該key保證整個(gè)App內(nèi)部都是唯一的募强,但是創(chuàng)建GlobalKey的代價(jià)非常昂貴,如果不需要保證整個(gè)App內(nèi)部唯一性崇摄,可以考慮使用LocalKey擎值。
LocalKey
與GlobalKey相對(duì)應(yīng)的一種局部key,需要保證創(chuàng)建LocalKey的Widget都有同一個(gè)父Widget逐抑,不然就失去其作用鸠儿。
UniqueKey
UniqueKey必須保證關(guān)聯(lián)的Widget只有一個(gè)child,屬于LocalKey厕氨。
ObjectKey
對(duì)象級(jí)別的key进每,通過Widget的實(shí)例對(duì)象來創(chuàng)建,與之類似的還有ValueKey命斧,它使用了類型來作為key值田晚,均屬于LocalKey。
訪問State
正如前文所述国葬,State與BuildContext相關(guān)聯(lián)贤徒,而BuildContext又與Widget相關(guān)聯(lián)。
Widget自身
在理論上是唯一能夠訪問state的對(duì)象汇四,而且state也可以直接訪問Widget的變/常量泞莉。
子Widget
某些時(shí)候父類Widget可能會(huì)需要訪問子類Widget的state中的值來完成一些特殊操作。
為了滿足這一需求船殉,最簡單的就是通過key來獲取鲫趁。針對(duì)以下代碼,我們可以使用myWidgetStateKey.currentState
來獲取state值:
...
GlobalKey<MyStatefulWidgetState> myWidgetStateKey = GlobalKey<MyStatefulWidgetState>();
...
@override
Widget build(BuildContext context){
return MyStatefulWidget(
key: myWidgetStateKey,
color: Colors.blue,
);
}
注意利虫,MyStatefulWidgetState類名沒有下劃線前綴挨厚,因?yàn)槲覀冃枰獙⑵浔┞冻鰜聿趴梢栽L問到。
父Widget
試想有以下Widget的樹形結(jié)構(gòu)糠惫,底層的子Widget想訪問根節(jié)點(diǎn)Widget的state:
想要達(dá)成這一目標(biāo)需要滿足3個(gè)條件:
- 根節(jié)點(diǎn)Widget需要暴露state變量疫剃,不再將state聲明為私有類型;
class MyExposingWidget extends StatefulWidget {
MyExposingWidgetState myState;
@override
MyExposingWidgetState createState(){
myState = MyExposingWidgetState();
return myState;
}
}
- State必須為其中的值創(chuàng)建getter/setter或者聲明值為
public
(不推薦)硼讽;
class MyExposingWidgetState extends State<MyExposingWidget>{
Color _color;
Color get color => _color;
...
}
- 底層Widget獲取到state的引用巢价。
class MyChildWidget extends StatelessWidget {
@override
Widget build(BuildContext context){
final MyExposingWidget widget = context.ancestorWidgetOfExactType(MyExposingWidget);
final MyExposingWidgetState state = widget?.myState;
return Container(
color: state == null ? Colors.blue : state.color,
);
}
}
雖然上述方案可以解決在任何地方訪問state的問題,但并不能感知state內(nèi)部的值何時(shí)進(jìn)行修改,隨之而來的Widget何時(shí)進(jìn)行重繪等問題壤躲,InheritedWidget就能幫助我們解決這一問題城菊。
4.InheritedWidget
簡而言之,InheritedWidget可以幫助我們?cè)赪idget樹形結(jié)構(gòu)中高效地傳遞數(shù)據(jù)信息碉克。作為一種特殊的Widget凌唬,它可以使Widget樹中所有的Widget都能夠共享數(shù)據(jù)。
基本概念
為了更加清楚的解釋相關(guān)概念漏麦,我們以下面代碼進(jìn)行說明:
class MyInheritedWidget extends InheritedWidget {
MyInheritedWidget({
Key key,
@required Widget child,
this.data,
}): super(key: key, child: child);
final data;
static MyInheritedWidget of(BuildContext context) {
return context.inheritFromWidgetOfExactType(MyInheritedWidget);
}
@override
bool updateShouldNotify(MyInheritedWidget oldWidget) => data != oldWidget.data;
}
代碼中定義了名為MyInheritedWidget
的Widget客税,它的目的即在其Widget子樹中共享其data變量。為了實(shí)現(xiàn)這一目的撕贞,我們還需要為它傳入子Widget作為構(gòu)造方法的參數(shù)更耻,才使得其子樹間的共享數(shù)據(jù)成為可能。換個(gè)更簡單的說法捏膨,如果想要某個(gè)Widget的子節(jié)點(diǎn)能共享數(shù)據(jù)酥夭,請(qǐng)使用InheritedWidget來"包裹"它。
再來看看靜態(tài)方法static MyInheritedWidget of(BuildContext context)
脊奋,則實(shí)現(xiàn)了從BuildContext中獲取具體類型Widget的功能。
最后疙描,重寫updateShouldNotify
方法來告知InheritedWidget的子Widget(訂閱/注冊(cè)過數(shù)據(jù)的修改通知)是否需要接收更新诚隙。
使用InheritedWidget時(shí),只需編寫類似如下代碼:
class MyParentWidget... {
...
@override
Widget build(BuildContext context){
return MyInheritedWidget(
data: counter,
child: Row(
children: <Widget>[
...
],
),
);
}
}
子Widget訪問數(shù)據(jù)
子Widget可以通過獲得InheritedWidget引用來訪問數(shù)據(jù):
class MyChildWidget... {
...
@override
Widget build(BuildContext context){
final MyInheritedWidget inheritedWidget = MyInheritedWidget.of(context);
///
/// From this moment, the widget can use the data, exposed by the MyInheritedWidget
/// by calling: inheritedWidget.data
///
return Container(
color: inheritedWidget.data.color,
);
}
}
Widget交互
試想有如下WIdget樹結(jié)構(gòu):
為了舉例說明圖中結(jié)構(gòu)起胰,我們假設(shè)以下場景:
- Widget A是將商品添加到購物車的按鈕久又;
- Widget B是展示購物車中商品數(shù)量的文本;
- Widget C是WIdget B同級(jí)的其他文本效五;
- 我們希望按下按鈕(Widget A)時(shí) Widget B能夠準(zhǔn)確顯示購物車中的商品數(shù)量地消,而Widget C并不會(huì)發(fā)生任何重繪。
為了模擬這一需求畏妖,我們編寫以下代碼:
class Item {
String reference;
Item(this.reference);
}
class _MyInherited extends InheritedWidget {
_MyInherited({
Key key,
@required Widget child,
@required this.data,
}) : super(key: key, child: child);
final MyInheritedWidgetState data;
@override
bool updateShouldNotify(_MyInherited oldWidget) {
return true;
}
}
class MyInheritedWidget extends StatefulWidget {
MyInheritedWidget({
Key key,
this.child,
}): super(key: key);
final Widget child;
@override
MyInheritedWidgetState createState() => MyInheritedWidgetState();
static MyInheritedWidgetState of(BuildContext context){
return (context.inheritFromWidgetOfExactType(_MyInherited) as _MyInherited).data;
}
}
class MyInheritedWidgetState extends State<MyInheritedWidget>{
/// List of Items
List<Item> _items = <Item>[];
/// Getter (number of items)
int get itemsCount => _items.length;
/// Helper method to add an Item
void addItem(String reference){
setState((){
_items.add( Item(reference));
});
}
@override
Widget build(BuildContext context){
return _MyInherited(
data: this,
child: widget.child,
);
}
}
class MyTree extends StatefulWidget {
@override
_MyTreeState createState() => _MyTreeState();
}
class _MyTreeState extends State<MyTree> {
@override
Widget build(BuildContext context) {
return MyInheritedWidget(
child: Scaffold(
appBar: AppBar(
title: Text('Title'),
),
body: Column(
children: <Widget>[
WidgetA(),
Container(
child: Row(
children: <Widget>[
Icon(Icons.shopping_cart),
WidgetB(),
WidgetC(),
],
),
),
],
),
),
);
}
}
class WidgetA extends StatelessWidget {
@override
Widget build(BuildContext context) {
final MyInheritedWidgetState state = MyInheritedWidget.of(context);
return Container(
child: RaisedButton(
child: Text('Add Item'),
onPressed: () {
state.addItem(' item');
},
),
);
}
}
class WidgetB extends StatelessWidget {
@override
Widget build(BuildContext context) {
final MyInheritedWidgetState state = MyInheritedWidget.of(context);
return Text('${state.itemsCount}');
}
}
class WidgetC extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Text('I am Widget C');
}
}
簡要說明一下每個(gè)類的功能:
-
_MyInherited
是一個(gè)InheritedWidget脉执,在點(diǎn)擊Widget A時(shí)會(huì)重復(fù)創(chuàng)建。 -
MyInheritedWidget
是一個(gè)StatefulWidget戒劫,其state管理著一個(gè)商品數(shù)組半夷,通過靜態(tài)方法static MyInheritedWidgetState of(BuildContext context)
來獲取state對(duì)象。 -
MyInheritedWidgetState
是管理商品數(shù)組的state類迅细,同時(shí)創(chuàng)建一個(gè)itemsCount
的getter方法以及addItem(String preference)
方便外部調(diào)用巫橄。State中每加入一個(gè)商品,build(BuildContext context)
方法會(huì)創(chuàng)建一個(gè)_MyInherited
對(duì)象茵典。 -
MyTree
創(chuàng)建了結(jié)構(gòu)圖中的Widget樹結(jié)構(gòu)湘换,并以MyInheritedWidget
為根節(jié)點(diǎn)。 -
WidgetA
是一個(gè)RaiseButton類型的Widget,點(diǎn)擊之后調(diào)用state的addItem(String preference)
方法以完成商品的添加操作彩倚。 -
WidgetB
是一個(gè)簡單的文本筹我,用于展示購物車中的商品數(shù)量。
那么署恍,代碼是如何實(shí)現(xiàn)Widget向State進(jìn)行注冊(cè)的呢崎溃?關(guān)鍵點(diǎn)就在于靜態(tài)方法static MyInheritedWidgetState of(BuildContext context)
的內(nèi)部實(shí)現(xiàn):
static MyInheritedWidgetState of(BuildContext context){
return (context.inheritFromWidgetOfExactType(_MyInherited) as _MyInherited).data;
}
當(dāng)子Widget調(diào)用該方法時(shí),會(huì)傳遞其BuildContext盯质,并返回對(duì)應(yīng)MyInheritedWidgetState
類型的實(shí)例對(duì)象袁串。如此一來,該方法完成了兩個(gè)目的:
- 作為數(shù)據(jù)消費(fèi)者的Widget被添加到訂閱者名單中呼巷,當(dāng)InheritedWidget管理的state發(fā)生數(shù)據(jù)變更時(shí)囱修,它會(huì)接收到通知以準(zhǔn)備重繪操作;
- 消費(fèi)者Widget會(huì)同時(shí)獲得數(shù)據(jù)管理者state的引用王悍。
數(shù)據(jù)流向
由于WidgetA
(RaiseButton)和WidgetB
(文本)均通過InheritedWidget訂閱更改破镰,所以任何傳遞到_MyInherited
更新的數(shù)據(jù)流向可以由以下(簡化)流程表示:
- 點(diǎn)擊按鈕之后,調(diào)用
MyInheritedWidgetState
的addItem
方法压储; -
addItem
方法添加一個(gè)新的Item到_items
中鲜漩; -
setState()
閉包調(diào)用以準(zhǔn)備MyInheritedWidget
的rebuild; - 執(zhí)行
build()
方法后集惋,包含data(_items
)的_MyInherited
對(duì)象被創(chuàng)建孕似; -
_MyInherited
通過構(gòu)造方法記錄新的state; -
_MyInherited
設(shè)置updateShouldNotify
回調(diào)為true以完成對(duì)訂閱者的通知刮刑; -
_MyInherited
遍歷所有訂閱者(包括WidgetA
和WidgetB
)喉祭,通知他們進(jìn)行rebuild; -
WidgetC
不是訂閱者雷绢,因此不會(huì)rebuild泛烙。
然而,這樣一來翘紊,WidgetA
和WidgetB
都會(huì)進(jìn)行rebuild蔽氨,但是WidgetA
自身并不需要rebuild,那如何防止訪問到InheritedWidget的部分Widget不rebuild呢帆疟?
其實(shí)孵滞,之所以會(huì)出現(xiàn)這樣的情況,原因在于context.inheritFromWidgetOfExactType()
方法會(huì)自動(dòng)將Widget作為訂閱鏈表上的一員鸯匹,要防止這種情況發(fā)生需修改為如下代碼:
static MyInheritedWidgetState of([BuildContext context, bool rebuild = true]) {
return (rebuild ? context.inheritFromWidgetOfExactType(_MyInherited) as _MyInherited
: context.ancestorWidgetOfExactType(_MyInherited) as _MyInherited).data;
}
然后修改WidgetA
如下:
class WidgetA extends StatelessWidget {
@override
Widget build(BuildContext context) {
final MyInheritedWidgetState state = MyInheritedWidget.of(context, false);
return new Container(
child: new RaisedButton(
child: new Text('Add Item'),
onPressed: () {
state.addItem('new item');
},
),
);
}
}