概念
在前面的介紹中,我們知道在Flutter中幾乎所有的對象都是一個Widget。與原生開發(fā)中“控件”不同的是流部,F(xiàn)lutter中的Widget的概念更廣泛雕憔,它不僅可以表示UI元素,也可以表示一些功能性的組件如:用于手勢檢測的 GestureDetector
widget廉赔、用于APP主題數(shù)據(jù)傳遞的Theme
等等,而原生開發(fā)中的控件通常只是指UI元素。在后面的內(nèi)容中音念,我們在描述UI元素時可能會用到“控件”、“組件”這樣的概念躏敢,讀者心里需要知道他們就是widget闷愤,只是在不同場景的不同表述而已。由于Flutter主要就是用于構(gòu)建用戶界面的件余,所以讥脐,在大多數(shù)時候,讀者可以認(rèn)為widget就是一個控件啼器,不必糾結(jié)于概念旬渠。
Widget與Element
在Flutter中,Widget的功能是“描述一個UI元素的配置數(shù)據(jù)”端壳,它就是說告丢,Widget其實(shí)并不是表示最終繪制在設(shè)備屏幕上的顯示元素,而它只是描述顯示元素的一個配置數(shù)據(jù)损谦。
實(shí)際上岖免,F(xiàn)lutter中真正代表屏幕上顯示元素的類是Element
岳颇,也就是說Widget只是描述Element
的配置數(shù)據(jù)!Widget只是UI元素的一個配置數(shù)據(jù)颅湘,并且一個Widget可以對應(yīng)多個Element
话侧。這是因?yàn)橥粋€Widget對象可以被添加到UI樹的不同部分,而真正渲染時闯参,UI樹的每一個Element
節(jié)點(diǎn)都會對應(yīng)一個Widget對象瞻鹏。總結(jié)一下:
- Widget實(shí)際上就是
Element
的配置數(shù)據(jù)鹿寨,Widget樹實(shí)際上是一個配置樹新博,而真正的UI渲染樹是由Element
構(gòu)成;不過脚草,由于Element
是通過Widget生成的叭披,所以它們之間有對應(yīng)關(guān)系,在大多數(shù)場景玩讳,我們可以寬泛地認(rèn)為Widget樹就是指UI控件樹或UI渲染樹涩蜘。 - 一個Widget對象可以對應(yīng)多個
Element
對象。這很好理解熏纯,根據(jù)同一份配置(Widget)同诫,可以創(chuàng)建多個實(shí)例(Element)。
Widget主要接口
我們先來看一下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
: 這個key
屬性類似于React/Vue中的key
秩贰,主要的作用是決定是否在下一次build
時復(fù)用舊的widget霹俺,決定的條件在canUpdate()
方法中。 -
createElement()
:正如前文所述“一個Widget可以對應(yīng)多個Element
”毒费;Flutter Framework在構(gòu)建UI樹時丙唧,會先調(diào)用此方法生成對應(yīng)節(jié)點(diǎn)的Element
對象。此方法是Flutter Framework隱式調(diào)用的觅玻,在我們開發(fā)過程中基本不會調(diào)用到想际。 -
debugFillProperties(...)
復(fù)寫父類的方法,主要是設(shè)置診斷樹的一些特性溪厘。 -
canUpdate(...)
是一個靜態(tài)方法胡本,它主要用于在Widget樹重新build
時復(fù)用舊的widget,其實(shí)具體來說畸悬,應(yīng)該是:是否用新的Widget對象去更新舊UI樹上所對應(yīng)的Element
對象的配置侧甫;通過其源碼我們可以看到,只要newWidget
與oldWidget
的runtimeType
和key
同時相等時就會用newWidget
去更新Element
對象的配置,否則就會創(chuàng)建新的Element
披粟。
另外Widget
類本身是一個抽象類彩扔,其中最核心的就是定義了createElement()
接口,在Flutter開發(fā)中僻爽,我們一般都不用直接繼承Widget
類來實(shí)現(xiàn)一個新組件,相反贾惦,我們通常會通過繼承StatelessWidget
或StatefulWidget
來間接繼承Widget
類來實(shí)現(xiàn)胸梆。StatelessWidget
和StatefulWidget
都是直接繼承自Widget
類,而這兩個類也正是Flutter中非常重要的兩個抽象類须板,它們引入了兩種Widget模型碰镜,接下來我們將重點(diǎn)介紹一下這兩個類。
StatelessWidget
StatelessWidget
相對比較簡單习瑰,它繼承自Widget
類绪颖,重寫了createElement()
方法:
@override
StatelessElement createElement() => new StatelessElement(this);
StatelessElement
間接繼承自Element
類,與StatelessWidget
相對應(yīng)(作為其配置數(shù)據(jù))甜奄。
StatelessWidget
用于不需要維護(hù)狀態(tài)的場景柠横,它通常在build
方法中通過嵌套其它Widget來構(gòu)建UI,在構(gòu)建過程中會遞歸的構(gòu)建其嵌套的Widget课兄。我們看一個簡單的例子:
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),
),
);
}
}
上面的代碼牍氛,實(shí)現(xiàn)了一個回顯字符串的Echo
widget。
按照慣例烟阐,
widget
的構(gòu)造函數(shù)參數(shù)應(yīng)使用命名參數(shù)搬俊,命名參數(shù)中的必要參數(shù)要添加@required
標(biāo)注,這樣有利于靜態(tài)代碼分析器進(jìn)行檢查蜒茄。另外唉擂,在繼承widget
時,第一個參數(shù)通常應(yīng)該是Key
檀葛,另外玩祟,如果Widget需要接收子Widget,那么child
或children
參數(shù)通常應(yīng)被放在參數(shù)列表的最后屿聋。同樣是按照慣例卵凑,Widget的屬性應(yīng)盡可能的被聲明為final
,防止被意外改變胜臊。
然后我們可以通過如下方式使用它:
Widget build(BuildContext context) {
return Echo(text: "hello world");
}
運(yùn)行后效果如圖
Context
build
方法有一個context
參數(shù)勺卢,它是BuildContext
類的一個實(shí)例,表示當(dāng)前widget在widget樹中的上下文象对,每一個widget都會對應(yīng)一個context對象(因?yàn)槊恳粋€widget都是widget樹上的一個節(jié)點(diǎn))黑忱。實(shí)際上,context
是當(dāng)前widget在widget樹中位置中執(zhí)行”相關(guān)操作“的一個句柄,比如它提供了從當(dāng)前widget開始向上遍歷widget樹以及按照widget類型查找父級widget的方法甫煞。下面是在子樹中獲取父級widget的一個示例:
class ContextRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Context測試"),
),
body: Container(
child: Builder(builder: (context) {
// 在Widget樹中向上查找最近的父級`Scaffold` widget
Scaffold scaffold = context.ancestorWidgetOfExactType(Scaffold);
// 直接返回 AppBar的title菇曲, 此處實(shí)際上是Text("Context測試")
return (scaffold.appBar as AppBar).title;
}),
),
);
}
}
運(yùn)行后效果如圖
StatefulWidget
和StatelessWidget
一樣,StatefulWidget
也是繼承自Widget
類抚吠,并重寫了createElement()
方法常潮,不同的是返回的Element
對象并不相同;另外StatefulWidget
類中添加了一個新的接口createState()
楷力。
下面我們看看StatefulWidget
的類定義:
abstract class StatefulWidget extends Widget {
const StatefulWidget({ Key key }) : super(key: key);
@override
StatefulElement createElement() => new StatefulElement(this);
@protected
State createState();
}
StatefulElement
間接繼承自Element
類喊式,與StatefulWidget相對應(yīng)(作為其配置數(shù)據(jù))。StatefulElement
中可能會多次調(diào)用createState()
來創(chuàng)建狀態(tài)(State)對象萧朝。createState()
用于創(chuàng)建和Stateful widget相關(guān)的狀態(tài)岔留,它在Stateful widget的生命周期中可能會被多次調(diào)用。例如检柬,當(dāng)一個Stateful widget同時插入到widget樹的多個位置時献联,F(xiàn)lutter framework就會調(diào)用該方法為每一個位置生成一個獨(dú)立的State實(shí)例,其實(shí)何址,本質(zhì)上就是一個StatefulElement
對應(yīng)一個State實(shí)例里逆。
State
一個StatefulWidget類會對應(yīng)一個State類,State表示與其對應(yīng)的StatefulWidget要維護(hù)的狀態(tài)用爪,State中的保存的狀態(tài)信息可以:
- 在widget 構(gòu)建時可以被同步讀取运悲。
- 在widget生命周期中可以被改變,當(dāng)State被改變時项钮,可以手動調(diào)用其
setState()
方法通知Flutter framework狀態(tài)發(fā)生改變班眯,F(xiàn)lutter framework在收到消息后,會重新調(diào)用其build
方法重新構(gòu)建widget樹烁巫,從而達(dá)到更新UI的目的署隘。
State中有兩個常用屬性:
widget
,它表示與該State實(shí)例關(guān)聯(lián)的widget實(shí)例亚隙,由Flutter framework動態(tài)設(shè)置磁餐。注意,這種關(guān)聯(lián)并非永久的阿弃,因?yàn)樵趹?yīng)用生命周期中诊霹,UI樹上的某一個節(jié)點(diǎn)的widget實(shí)例在重新構(gòu)建時可能會變化,但State實(shí)例只會在第一次插入到樹中時被創(chuàng)建渣淳,當(dāng)在重新構(gòu)建時脾还,如果widget被修改了,F(xiàn)lutter framework會動態(tài)設(shè)置State.widget為新的widget實(shí)例入愧。context
鄙漏。StatefulWidget對應(yīng)的BuildContext嗤谚,作用同StatelessWidget的BuildContext。
State生命周期
理解State的生命周期對flutter開發(fā)非常重要怔蚌,為了加深讀者印象巩步,本節(jié)我們通過一個實(shí)例來演示一下State的生命周期。在接下來的示例中桦踊,我們實(shí)現(xiàn)一個計(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
接收一個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)建一個新路由术徊,在新路由中本刽,我們只顯示一個CounterWidget
:
Widget build(BuildContext context) {
return CounterWidget();
}
我們運(yùn)行應(yīng)用并打開該路由頁面,在新路由頁打開后赠涮,屏幕中央就會出現(xiàn)一個數(shù)字0子寓,然后控制臺日志輸出:
I/flutter ( 5436): initState
I/flutter ( 5436): didChangeDependencies
I/flutter ( 5436): build
可以看到,在StatefulWidget插入到Widget樹時首先initState
方法會被調(diào)用笋除。
然后我們點(diǎn)擊??按鈕熱重載斜友,控制臺輸出日志如下:
I/flutter ( 5436): reassemble
I/flutter ( 5436): didUpdateWidget
I/flutter ( 5436): build
可以看到此時initState
和didChangeDependencies
都沒有被調(diào)用,而此時didUpdateWidget
被調(diào)用垃它。
接下來鲜屏,我們在widget樹中移除CounterWidget
,將路由build
方法改為:
Widget build(BuildContext context) {
//移除計(jì)數(shù)器
//return CounterWidget();
//隨便返回一個Text()
return Text("xxx");
}
然后熱重載国拇,日志如下:
I/flutter ( 5436): reassemble
I/flutter ( 5436): deactive
I/flutter ( 5436): dispose
我們可以看到洛史,在CounterWidget
從widget樹中移除時,deactive
和dispose
會依次被調(diào)用酱吝。
下面我們來看看各個回調(diào)函數(shù):
initState
:當(dāng)Widget第一次插入到Widget樹時會被調(diào)用也殖,對于每一個State對象,F(xiàn)lutter framework只會調(diào)用一次該回調(diào)务热,所以忆嗜,通常在該回調(diào)中做一些一次性的操作,如狀態(tài)初始化崎岂、訂閱子樹的事件通知等捆毫。不能在該回調(diào)中調(diào)用BuildContext.inheritFromWidgetOfExactType
(該方法用于在Widget樹上獲取離當(dāng)前widget最近的一個父級InheritFromWidget
,關(guān)于InheritedWidget
我們將在后面章節(jié)介紹)冲甘,原因是在初始化完成后冻璃,Widget樹中的InheritFromWidget
也可能會發(fā)生變化响谓,所以正確的做法應(yīng)該在build()
方法或didChangeDependencies()
中調(diào)用它。didChangeDependencies()
:當(dāng)State對象的依賴發(fā)生變化時會被調(diào)用省艳;例如:在之前build()
中包含了一個InheritedWidget
娘纷,然后在之后的build()
中InheritedWidget
發(fā)生了變化,那么此時InheritedWidget
的子widget的didChangeDependencies()
回調(diào)都會被調(diào)用跋炕。典型的場景是當(dāng)系統(tǒng)語言Locale或應(yīng)用主題改變時赖晶,F(xiàn)lutter framework會通知widget調(diào)用此回調(diào)。-
build()
:此回調(diào)讀者現(xiàn)在應(yīng)該已經(jīng)相當(dāng)熟悉了辐烂,它主要是用于構(gòu)建Widget子樹的遏插,會在如下場景被調(diào)用:- 在調(diào)用
initState()
之后。 - 在調(diào)用
didUpdateWidget()
之后纠修。 - 在調(diào)用
setState()
之后胳嘲。 - 在調(diào)用
didChangeDependencies()
之后。 - 在State對象從樹中一個位置移除后(會調(diào)用deactivate)又重新插入到樹的其它位置之后扣草。
- 在調(diào)用
reassemble()
:此回調(diào)是專門為了開發(fā)調(diào)試而提供的了牛,在熱重載(hot reload)時會被調(diào)用,此回調(diào)在Release模式下永遠(yuǎn)不會被調(diào)用辰妙。didUpdateWidget()
:在widget重新構(gòu)建時鹰祸,F(xiàn)lutter framework會調(diào)用Widget.canUpdate
來檢測Widget樹中同一位置的新舊節(jié)點(diǎn),然后決定是否需要更新密浑,如果Widget.canUpdate
返回true
則會調(diào)用此回調(diào)蛙婴。正如之前所述,Widget.canUpdate
會在新舊widget的key和runtimeType同時相等時會返回true尔破,也就是說在在新舊widget的key和runtimeType同時相等時didUpdateWidget()
就會被調(diào)用街图。deactivate()
:當(dāng)State對象從樹中被移除時,會調(diào)用此回調(diào)懒构。在一些場景下台夺,F(xiàn)lutter framework會將State對象重新插到樹中,如包含此State對象的子樹在樹的一個位置移動到另一個位置時(可以通過GlobalKey來實(shí)現(xiàn))痴脾。如果移除后沒有重新插入到樹中則緊接著會調(diào)用dispose()
方法颤介。dispose()
:當(dāng)State對象從樹中被永久移除時調(diào)用;通常在此回調(diào)中釋放資源赞赖。
StatefulWidget生命周期如圖所示: