概念
在前面的介紹中隔披,我們知道在Flutter中幾乎所有的對(duì)象都是一個(gè)Widget收津。與原生開發(fā)中“控件”不同的是裳扯,F(xiàn)lutter中的Widget的概念更廣泛澄耍,它不僅可以表示UI元素噪珊,也可以表示一些功能性的組件如:用于手勢(shì)檢測(cè)的 GestureDetector
widget晌缘、用于APP主題數(shù)據(jù)傳遞的Theme
等等,而原生開發(fā)中的控件通常只是指UI元素痢站。在后面的內(nèi)容中磷箕,我們?cè)诿枋鯱I元素時(shí)可能會(huì)用到“控件”、“組件”這樣的概念阵难,讀者心里需要知道他們就是widget岳枷,只是在不同場(chǎng)景的不同表述而已。由于Flutter主要就是用于構(gòu)建用戶界面的呜叫,所以空繁,在大多數(shù)時(shí)候,讀者可以認(rèn)為widget就是一個(gè)控件朱庆,不必糾結(jié)于概念盛泡。
Widget與Element
在Flutter中,Widget的功能是“描述一個(gè)UI元素的配置數(shù)據(jù)”椎工,它就是說饭于,Widget其實(shí)并不是表示最終繪制在設(shè)備屏幕上的顯示元素蜀踏,而它只是描述顯示元素的一個(gè)配置數(shù)據(jù)维蒙。
實(shí)際上,F(xiàn)lutter中真正代表屏幕上顯示元素的類是Element
果覆,也就是說Widget只是描述Element
的配置數(shù)據(jù)颅痊!Widget只是UI元素的一個(gè)配置數(shù)據(jù),并且一個(gè)Widget可以對(duì)應(yīng)多個(gè)Element局待。這是因?yàn)橥粋€(gè)Widget對(duì)象可以被添加到UI樹的不同部分斑响,而真正渲染時(shí),UI樹的每一個(gè)Element
節(jié)點(diǎn)都會(huì)對(duì)應(yīng)一個(gè)Widget對(duì)象钳榨〗⒎#總結(jié)一下:
Widget實(shí)際上就是Element
的配置數(shù)據(jù),Widget樹實(shí)際上是一個(gè)配置樹薛耻,而真正的UI渲染樹是由Element
構(gòu)成营罢;不過,由于Element
是通過Widget生成的饼齿,所以它們之間有對(duì)應(yīng)關(guān)系饲漾,在大多數(shù)場(chǎng)景,我們可以寬泛地認(rèn)為Widget樹就是指UI控件樹或UI渲染樹缕溉。
一個(gè)Widget對(duì)象可以對(duì)應(yīng)多個(gè)Element對(duì)象考传。這很好理解,根據(jù)同一份配置(Widget)证鸥,可以創(chuàng)建多個(gè)實(shí)例(Element)僚楞。
讀者應(yīng)該將這兩點(diǎn)牢記在心中勤晚。
Widget主要接口
@immutable
abstract class Widget extends DiagnosticableTree {
const Widget({ this.key });
final Key key;
@protected
Element createElement();
@override
String toStringShort() {
return key == null ? '$runtimeType' : '$runtimeType-$key';
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.defaultDiagnosticsTreeStyle = DiagnosticsTreeStyle.dense;
}
static bool canUpdate(Widget oldWidget, Widget newWidget) {
return oldWidget.runtimeType == newWidget.runtimeType
&& oldWidget.key == newWidget.key;
}
}
Widget
類繼承自DiagnosticableTree
,DiagnosticableTree
即“診斷樹”泉褐,主要作用是提供調(diào)試信息运翼。
Key
: 這個(gè)key
屬性類似于React/Vue中的key
,主要的作用是決定是否在下一次build時(shí)復(fù)用舊的widget
兴枯,決定的條件在canUpdate()
方法中血淌。
createElement()
:正如前文所述“一個(gè)Widget
可以對(duì)應(yīng)多個(gè)Elemen
t”;Flutter Framework在構(gòu)建UI樹時(shí)财剖,會(huì)先調(diào)用此方法生成對(duì)應(yīng)節(jié)點(diǎn)的Element
對(duì)象悠夯。此方法是Flutter Framework隱式調(diào)用的,在我們開發(fā)過程中基本不會(huì)調(diào)用到躺坟。
debugFillProperties(...)
復(fù)寫父類的方法沦补,主要是設(shè)置診斷樹的一些特性。
canUpdate(...)
是一個(gè)靜態(tài)方法咪橙,它主要用于在Widget
樹重新build時(shí)復(fù)用舊的widget
夕膀,其實(shí)具體來說,應(yīng)該是:是否用新的Widget
對(duì)象去更新舊UI樹上所對(duì)應(yīng)的Element
對(duì)象的配置美侦;通過其源碼我們可以看到产舞,只要newWidget
與oldWidget
的runtimeType
和key
同時(shí)相等時(shí)就會(huì)用newWidget
去更新Element
對(duì)象的配置,否則就會(huì)創(chuàng)建新的Element
菠剩。
另外Widget
類本身是一個(gè)抽象類易猫,其中最核心的就是定義了createElement()
接口,在Flutter開發(fā)中具壮,我們一般都不用直接繼承Widget
類來實(shí)現(xiàn)一個(gè)新組件准颓,相反,我們通常會(huì)通過繼承StatelessWidget
或StatefulWidget
來間接繼承Widget
類來實(shí)現(xiàn)棺妓。StatelessWidget
和StatefulWidget
都是直接繼承自Widget
類攘已,而這兩個(gè)類也正是Flutter中非常重要的兩個(gè)抽象類,它們引入了兩種Widget
模型怜跑,接下來我們將重點(diǎn)介紹一下這兩個(gè)類样勃。
StatelessWidget
我們已經(jīng)簡(jiǎn)單介紹過StatelessWidget
,StatelessWidget
相對(duì)比較簡(jiǎn)單妆艘,它繼承自Widget
類彤灶,重寫了createElement()
方法:
@override
StatelessElement createElement() => new StatelessElement(this);
StatelessElement
間接繼承自Element
類,與StatelessWidget
相對(duì)應(yīng)(作為其配置數(shù)據(jù))批旺。
StatelessWidget
用于不需要維護(hù)狀態(tài)的場(chǎng)景幌陕,它通常在build方法中通過嵌套其它Widget
來構(gòu)建UI,在構(gòu)建過程中會(huì)遞歸的構(gòu)建其嵌套的Widget
汽煮。我們看一個(gè)簡(jiǎn)單的例子:
class Echo extends StatelessWidget {
const Echo({
Key key,
@required this.text,
this.backgroundColor:Colors.grey,
}):super(key:key);
final String text;
final Color backgroundColor;
@override
Widget build(BuildContext context) {
return Center(
child: Container(
color: backgroundColor,
child: Text(text),
),
);
}
}
然后我們可以通過如下方式使用它:
Widget build(BuildContext context) {
return Echo(text: "hello world");
}
StatefulWidget
和StatelessWidget
一樣搏熄,StatefulWidget
也是繼承自Widget
類棚唆,并重寫了createElement()
方法,不同的是返回的Element
對(duì)象并不相同心例;另外StatefulWidget
類中添加了一個(gè)新的接口createState()
宵凌。
abstract class StatefulWidget extends Widget {
const StatefulWidget({ Key key }) : super(key: key);
@override
StatefulElement createElement() => new StatefulElement(this);
@protected
State createState();
}
StatefulElement
間接繼承自Element
類,與StatefulWidget
相對(duì)應(yīng)(作為其配置數(shù)據(jù))止后。StatefulElement
中可能會(huì)多次調(diào)用createState()
來創(chuàng)建狀態(tài)(State
)對(duì)象瞎惫。
createState()
用于創(chuàng)建和Stateful widget
相關(guān)的狀態(tài),它在Stateful widget
的生命周期中可能會(huì)被多次調(diào)用译株。例如瓜喇,當(dāng)一個(gè)Stateful widget
同時(shí)插入到widget
樹的多個(gè)位置時(shí),F(xiàn)lutter framework就會(huì)調(diào)用該方法為每一個(gè)位置生成一個(gè)獨(dú)立的State
實(shí)例歉糜,其實(shí)乘寒,本質(zhì)上就是一個(gè)StatefulElement
對(duì)應(yīng)一個(gè)State
實(shí)例。
State
一個(gè)StatefulWidget
類會(huì)對(duì)應(yīng)一個(gè)State
類匪补,State
表示與其對(duì)應(yīng)的StatefulWidget
要維護(hù)的狀態(tài)伞辛,State
中的保存的狀態(tài)信息可以:
- 在
widget
構(gòu)建時(shí)可以被同步讀取。 - 在
widget
生命周期中可以被改變夯缺,當(dāng)State
被改變時(shí)蚤氏,可以手動(dòng)調(diào)用其setState()
方法通知Flutter framework狀態(tài)發(fā)生改變喳逛,F(xiàn)lutter framework在收到消息后,會(huì)重新調(diào)用其build
方法重新構(gòu)建widget
樹润文,從而達(dá)到更新UI的目的殿怜。
State中有兩個(gè)常用屬性:
1.widget
,它表示與該State
實(shí)例關(guān)聯(lián)的widget
實(shí)例头谜,由Flutter framework動(dòng)態(tài)設(shè)置。注意柱告,這種關(guān)聯(lián)并非永久的,因?yàn)樵趹?yīng)用生命周期中际度,UI樹上的某一個(gè)節(jié)點(diǎn)的widget
實(shí)例在重新構(gòu)建時(shí)可能會(huì)變化,但State
實(shí)例只會(huì)在第一次插入到樹中時(shí)被創(chuàng)建乖菱,當(dāng)在重新構(gòu)建時(shí)蓬网,如果widget
被修改了鹉勒,F(xiàn)lutter framework會(huì)動(dòng)態(tài)設(shè)置State.widget
為新的widget
實(shí)例。
2.context
禽额。StatefulWidget
對(duì)應(yīng)的BuildContext
,作用同StatelessWidget
的BuildContext
脯倒。
State生命周期
理解State
的生命周期對(duì)flutter開發(fā)非常重要哲鸳,為了加深讀者印象,本節(jié)我們通過一個(gè)實(shí)例來演示一下State
的生命周期盔憨。在接下來的示例中徙菠,我們實(shí)現(xiàn)一個(gè)計(jì)數(shù)器widget,點(diǎn)擊它可以使計(jì)數(shù)器加1郁岩,由于要保存計(jì)數(shù)器的數(shù)值狀態(tài)婿奔,所以我們應(yīng)繼承StatefulWidget
,代碼如下:
class CounterWidget extends StatefulWidget {
const CounterWidget({
Key key,
this.initValue: 0
});
final int initValue;
@override
_CounterWidgetState createState() => new _CounterWidgetState();
}
CounterWidget
接收一個(gè)initValue
整型參數(shù)问慎,它表示計(jì)數(shù)器的初始值萍摊。下面我們看一下State
的代碼:
class _CounterWidgetState extends State<CounterWidget> {
int _counter;
@override
void initState() {
super.initState();
//初始化狀態(tài)
_counter=widget.initValue;
print("initState");
}
@override
Widget build(BuildContext context) {
print("build");
return Scaffold(
body: Center(
child: FlatButton(
child: Text('$_counter'),
//點(diǎn)擊后計(jì)數(shù)器自增
onPressed:()=>setState(()=> ++_counter,
),
),
),
);
}
@override
void didUpdateWidget(CounterWidget oldWidget) {
super.didUpdateWidget(oldWidget);
print("didUpdateWidget");
}
@override
void deactivate() {
super.deactivate();
print("deactive");
}
@override
void dispose() {
super.dispose();
print("dispose");
}
@override
void reassemble() {
super.reassemble();
print("reassemble");
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
print("didChangeDependencies");
}
}
接下來,我們創(chuàng)建一個(gè)新路由如叼,在新路由中冰木,我們只顯示一個(gè)CounterWidget
:
Widget build(BuildContext context) {
return CounterWidget();
}
我們運(yùn)行應(yīng)用并打開該路由頁面,在新路由頁打開后笼恰,屏幕中央就會(huì)出現(xiàn)一個(gè)數(shù)字0踊沸,然后控制臺(tái)日志輸出:
I/flutter ( 5436): initState
I/flutter ( 5436): didChangeDependencies
I/flutter ( 5436): build
可以看到,在StatefulWidget插入到Widget樹時(shí)首先initState
方法會(huì)被調(diào)用社证。
然后我們點(diǎn)擊??按鈕熱重載逼龟,控制臺(tái)輸出日志如下:
I/flutter ( 5436): reassemble
I/flutter ( 5436): didUpdateWidget
I/flutter ( 5436): build
可以看到此時(shí)initState
和didChangeDependencies
都沒有被調(diào)用,而此時(shí)didUpdateWidget
被調(diào)用追葡。
接下來腺律,我們?cè)?code>widget樹中移除CounterWidget
,將路由build
方法改為:
Widget build(BuildContext context) {
//移除計(jì)數(shù)器
//return CounterWidget();
//隨便返回一個(gè)Text()
return Text("xxx");
}
然后熱重載宜肉,日志如下:
I/flutter ( 5436): reassemble
I/flutter ( 5436): deactive
I/flutter ( 5436): dispose
我們可以看到匀钧,在CounterWidget
從widget
樹中移除時(shí),deactive
和dispose
會(huì)依次被調(diào)用谬返。
下面我們來看看各個(gè)回調(diào)函數(shù):
initState
:當(dāng)Widget
第一次插入到Widget
樹時(shí)會(huì)被調(diào)用之斯,對(duì)于每一個(gè)State
對(duì)象,F(xiàn)lutter framework只會(huì)調(diào)用一次該回調(diào)朱浴,所以吊圾,通常在該回調(diào)中做一些一次性的操作达椰,如狀態(tài)初始化、訂閱子樹的事件通知等项乒。不能在該回調(diào)中調(diào)用BuildContext.dependOnInheritedWidgetOfExactType(該方法用于在Widge
t樹上獲取離當(dāng)前widget
最近的一個(gè)父級(jí)InheritFromWidget
啰劲,關(guān)于InheritedWidget
我們將在后面章節(jié)介紹),原因是在初始化完成后檀何,Widget
樹中的InheritFromWidget
也可能會(huì)發(fā)生變化蝇裤,所以正確的做法應(yīng)該在在build()
方法或didChangeDependencies()
中調(diào)用它栓辜。didChangeDependencies()
:當(dāng)State
對(duì)象的依賴發(fā)生變化時(shí)會(huì)被調(diào)用;例如:在之前build()
中包含了一個(gè)InheritedWidget
藕甩,然后在之后的build()
中InheritedWidget
發(fā)生了變化狭莱,那么此時(shí)InheritedWidget
的子widget
的didChangeDependencies()
回調(diào)都會(huì)被調(diào)用概作。典型的場(chǎng)景是當(dāng)系統(tǒng)語言Locale或應(yīng)用主題改變時(shí)讯榕,F(xiàn)lutter framework會(huì)通知widget調(diào)用此回調(diào)。-
build()
:此回調(diào)讀者現(xiàn)在應(yīng)該已經(jīng)相當(dāng)熟悉了济竹,它主要是用于構(gòu)建Widget子樹的规辱,會(huì)在如下場(chǎng)景被調(diào)用:1.在調(diào)用
initState()
之后栽燕。
2.在調(diào)用didUpdateWidget()
之后碍岔。
3.在調(diào)用setState()
之后蔼啦。
4.在調(diào)用didChangeDependencies()
之后捏肢。
5.在State
對(duì)象從樹中一個(gè)位置移除后(會(huì)調(diào)用deactivate
)又重新插入到樹的其它位置之后。 reassemble()
:此回調(diào)是專門為了開發(fā)調(diào)試而提供的衣屏,在熱重載(hot reload)時(shí)會(huì)被調(diào)用辩棒,此回調(diào)在Release模式下永遠(yuǎn)不會(huì)被調(diào)用一睁。didUpdateWidget()
:在widget
重新構(gòu)建時(shí),F(xiàn)lutter framework會(huì)調(diào)用Widget.canUpdate
來檢測(cè)Widget
樹中同一位置的新舊節(jié)點(diǎn)窘俺,然后決定是否需要更新瘤泪,如果Widget.canUpdate
返回true則會(huì)調(diào)用此回調(diào)染坯。正如之前所述单鹿,Widget.canUpdate
會(huì)在新舊widget
的key
和runtimeType
同時(shí)相等時(shí)會(huì)返回true,也就是說在在新舊widget
的key
和runtimeType
同時(shí)相等時(shí)didUpdateWidget()
就會(huì)被調(diào)用劲妙。deactivate()
:當(dāng)State
對(duì)象從樹中被移除時(shí)镣奋,會(huì)調(diào)用此回調(diào)侨颈。在一些場(chǎng)景下芯义,F(xiàn)lutter framework會(huì)將State對(duì)象重新插到樹中扛拨,如包含此State對(duì)象的子樹在樹的一個(gè)位置移動(dòng)到另一個(gè)位置時(shí)(可以通過GlobalKey來實(shí)現(xiàn))。如果移除后沒有重新插入到樹中則緊接著會(huì)調(diào)用dispose()
方法求泰。-
dispose()
:當(dāng)State對(duì)象從樹中被永久移除時(shí)調(diào)用渴频;通常在此回調(diào)中釋放資源。
StatefulWidget生命周期如圖3-2所示:
注意:在繼承StatefulWidget重寫其方法時(shí)志衍,對(duì)于包含@mustCallSuper標(biāo)注的父類方法楼肪,都要在子類方法中先調(diào)用父類方法春叫。