首先,在Flutter中幾乎所有的對象都是一個Widget。
跟原生開發中的“控件”不同,Flutter中的Widget的概念更加廣泛,它不僅可以表示UI元素,也可以表示一些功能性的組件:用于手勢監測的 GestureDetector widget,用于App 主題數據傳遞的Theme等等。
本文主要是介紹 Widget StatelessWidget 和StatefulWidget,在開發過程中,我們經常通過繼承StatelessWidget或者StatefuleWidget自定義Widget。
Widget和Element
在Flutter中,Widget的功能時“描述一個UI元素的配置數據”,它就是說,Widget其實并不是表示最終繪制在設備屏幕上的顯示元素,它只是描述顯示元素的一個配置數據。
實際上,Flutter中真正代表屏幕上顯示元素的類是 Element,也就是說Widget只是描述 Element的配置數據。
Widget只是UI元素的一個配置數據,并且一個Widget可以對應多個Element,這是因為同一個Widget對象可以被添加到UI樹的不同部分,而真正渲染時,UI樹的每個Element節點都會對應一個Widget對象,總結如下:
- widget實際上就是Element的配置數據,Widget樹實際上是一個配置數,而真正的UI渲染樹是由Element構成;不過,由于Element是通過Widget生成的,所以它們之間有對應關系,在大多數場景,我們可以寬泛地認為Widget樹就是指UI控件樹或者UI渲染樹。
- 一個Widget對象可以對應多個Element對象。這個很容易理解,根據同一份配置(Widget),可以創建多個實例(Element)。
Widget 類
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 即“診斷樹”,主要作用是提供調試信息。 - Key
key用于控制控件如何取代樹中的另一個控件.
如果這兩個控件的runtimeType和key屬性分別operator==(這里我理解為兩個控件的runtimeType和key分別相等,不知道對不對?), 那么新的控件通過更新底層元素來替換舊的控件(通過調用Element.update). 否則舊的控件將從樹上刪除, element會生成新的控件, 然后新的element會被插入到樹中.
另外,使用GlobalKey作為控件的key,允許element在樹周圍移動(改變父節點),而不會丟失狀態。當找到一個新的Widget時,(它的key和type與同一位置的前一個Widget不匹配),但是在前面的結構中某個地方有一個具有相同全局鍵的Widget,那么該Widget的元素將被移動到新的位置。
通常,作為另一個Widget的唯一子節點的Widget不需要一個明確的Key。 - Element createElement()
將本身的配置擴展到具體實例(Element)。
上面說過Widget只是描述Element的配置數據,這個方法就是通過Widget生成一個Element。通過一個Widget可以生成多個Element。
Flutter Framework在構建UI樹時,會先調用此方法生成對應節點的Element對象,這個方法是Flutter Framework隱式調用的,開發時基本不會用到。 - debugFillProperties()
復寫父類的方法,主要是設置診斷樹的一些特性。 - canUpdate()
是一個靜態方法,它主要用于在Widget樹重新build時復用舊的widget,具體來說應該是:是否用新的Widget對象去更新舊UI樹上所對應的Element對象的配置;我們可以在其源碼中看到,只要newWidget和oldWidget的runtimeType和key同時相等時就會用newWidget去更新Element對象的配置,否則就會創建新的Element。
通過查看Widget類的代碼,我們可以總結如下:
- Widget是一個抽象類
- 最核心的就是定義了 createElement()接口,開發中我們一般不直接繼承Widget,而是通過繼承StatelessWidget或StatefulWidget來間接繼承Widget類。
StatelessWidget
StatelessWidget用于不需要維護狀態的場景,它通常在build方法中通過嵌套其它Widget來構建UI,過程中會遞歸的構建其嵌套的Widget。
StatelessWidget比較簡單,它繼承自Widget類,并且重寫了createElement() 方法,我們可以看下:
abstract class StatelessWidget extends Widget {
StatelessElement createElement() => StatelessElement(this);
Widget build(BuildContext context);
}
- StatelessElement createElement() => StatelessElement(this);
StatelessElement間接繼承自Element類,與StatelessWidget相對應(作為其配置數據)。 - Widget build(BuildContext context);
這個方法就是需要返回一個新創建的Widget,比如Text,Image,這個Widget由其構造函數和BuildContext的信息配置。給定的BuildContext中包含了關于構建Widget所在樹中的位置的信息。
當這個StatelessWidget插入到給定[BuildContext]樹中的時候,或者依賴項發生更改的時候(比如由該小部件引用的[InheritedWidget]發生更改時)會調用此方法。
這個context,表示當前widget在widget樹中的上下文,每個widget都會對應一個context對象(因為每個widget都是widget樹上的一個節點)。實際上,context是當前widget在widget樹中位置執行“相關操作”的一個句柄,比如它提供了從當前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, 此處實際上是Text("Context測試")
return (scaffold.appBar as AppBar).title;
}),
),
);
}
}
使用繼承StatelessWidget自定義widget的簡單的例子:
class Echo extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Center(
child: Container(
color: UIColors.red,
child: Text("這是一個文本組件"),
),
);
}
}
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相對應(作為其配置數據)。StatefulElement中可能會多次調用 createState()來創建狀態(State)對象。
- createState() 用于創建和Stateful widget相關的狀態,它在Stateful widget的生命周期中可能會被多次調用。例如,當一個Stateful widget同時插入到widget樹的多個位置時,Flutter framwork就會調用該方法為每一個位置生成一個獨立的State實例,其實,本質上就是一個StatefulElement對應一個State實例。
State
一個StatefulWidget類會對應一個State類,State表示與其對應的StatefulWidget要維護的狀態,State中的保存的狀態信息可以:
- 在widget構建時可以被同步讀取
- 在widget生命周期中可以被改變,當State被改變時,可以手動調用其setState()方法通知Flutter framework狀態發生改變,Flutter framework在收到消息后,會重新調用其 build() 方法重新構建widget樹,從而達到更新UI的目的。
State中有兩個常用屬性:
- widget,它表示與該State實例關聯的widget實例,由Flutter framework動態設置。注意,這種關聯并非永久的,因為在應用聲明周期中,UI樹上的某一個節點的widget實例在重新構建時可能會變化,但是State實例只會在第一次插入到樹中時被創建,當在重新構建時,如果widget被修改了,Flutter framework會動態設置State.widget為新的widget實例。
- context,StatefulWidget對應的BuildContext,作用同StatelessWidget的BuildContext。
State生命周期
這里使用一個實例來演示一下State的生命周期,實現一個計數器widget,點擊它可以使計數器加1,由于要保存計數器的數值狀態,所以我們應該繼承StatefulWidget,代碼如下:
class CounterWidget extends StatefulWidget {
const CounterWidget({
Key key,
this.initValue: 0
});
final int initValue;
@override
_CounterWidgetState createState() => new _CounterWidgetState();
}
CounterWidget接收一個initValue整型參數,它表示計數器的初始值,下面我們看下State的代碼:
class _CounterWidgetState extends State<CounterWidget> {
int _counter;
@override
void initState() {
super.initState();
//初始化狀態
_counter=widget.initValue;
print("initState");
}
@override
Widget build(BuildContext context) {
print("build");
return Scaffold(
body: Center(
child: FlatButton(
child: Text('$_counter'),
//點擊后計數器自增
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");
}
}
接下來創建一個新路由,在新路由中,我們只顯示一個CounterWidget:
Widget build(BuildContext context) {
return CounterWidget();
}
我們運行應用并打開該路由頁面,在新路由頁打開后,屏幕中央就會出現一個數字0,然后控制臺日志輸出:
I/flutter ( 5436): initState
I/flutter ( 5436): didChangeDependencies
I/flutter ( 5436): build
可以看到,在StatefulWidget插入到Widget樹時首先initState方法會被調用。
然后我們點擊??按鈕熱重載,控制臺輸出日志如下:
I/flutter ( 5436): reassemble
I/flutter ( 5436): didUpdateWidget
I/flutter ( 5436): build
可以看到此時initState 和didChangeDependencies都沒有被調用,而此時didUpdateWidget被調用。
接下來,我們在widget樹中移除CounterWidget,將路由build方法改為:
Widget build(BuildContext context) {
//移除計數器
//return CounterWidget();
//隨便返回一個Text()
return Text("xxx");
}
然后熱重載,日志如下:
I/flutter ( 5436): reassemble
I/flutter ( 5436): deactive
I/flutter ( 5436): dispose
我們可以看到,在CounterWidget從widget樹中移除時,deactive和dispose會依次被調用。
下面我們來看看各個回調函數:
- initState
當widget第一次插入到widget樹時會被調用,對于每一個State對象,Flutter framework只會調用一次該回調,所以,通常在該回調中做一些一次性的操作,如狀態初始化、訂閱子樹的事件通知等。不能在該回調中調用BuildContext.inheritFromWidgetOfExactType(該方法用于在widget樹上獲取離當親widget最近的一個父級InheritFromWidget),原因是在初始化完成后,widget樹中的InheritFromWidget也可能會發生變化,所以正確的做法應該在build()或 didchangeDependencies()中調用用它。 - didChangeDependencies()
當State對象的依賴發生變化時會被調用;例如在之前build()中包含了一個InheritedWidget,然后在之后的build()中InheritedWidget發生了變化,那么此時InheritedWidget的子widget的didChangeDependencies()回調都會被調用。典型的場景就是當系統語言Locale或應用主題改變時,Flutter framework會通知widget調用此回調。 - build()
主要是用于構建widget子樹的,會在如下場景被調用:
1、在調用initState()之后
2、在調用didUpdateWidget()之后
3、在調用setState()之后
4、在調用didChangeDependencies()之后
5、在State對象從樹中一個位置移除后(會調用deactivate)又重新插入到樹的其它位置之后。 - reassemble
這個回調是專門為了開發調試而提供的,在熱重載(hot reload)時會被調用,此回調在release模式下永遠不會被調用 - didUpdateWidget()
在widget重新構建時,Flutter framework會調用 Widget.canUpdate 來檢測Widget樹中同一位置的新舊節點,然后決定是否需要更新,如果Widget.canUpdate 返回true則會調用此回調。正如之前所述,Widget.canUpdate會在新舊widget的key和runtimeType同時相等時會返回true,也就是說在新舊widget的key和runtimeType同時相等時 didUpdateWidget() 就會被調用。 - deactivate()
當state對象從樹中被移除時,會調用此回調。在一些場景下,Flutter framework會將State對象重新插入到樹中,如包含此State對象的子樹在樹的一個位置移動到另一個位置時(可以通過GlobalKey來實現)。如果移除后沒有重新插入到樹中則緊接著會調用dispose() 方法。 - dispose
當State對象從樹中被永久移除時調用;通常在此會調用釋放資源。
StatefulWidget生命周期如圖所示:
Tips:在繼承StatefulWidget重寫其方法時,對于包含@mustCallSuper標注的父類方法,都要在子類方法中先調用父類方法。
為什么要將build方法放在State中,而不是放在StatefulWidget中?
為什么build方法放在State 而不是StatefulWidget中?這主要是為了提高開發的靈活性。如果將build()方法在StatefulWidget中則會有兩個問題:
- 狀態訪問不便
如果StatefulWidget有很多狀態,每次狀態改變都要調用build方法,由于狀態是保存在State中的,如果build方法在StatefulWidget中,那么build方法和狀態分別在兩個類中,那么構建時讀取狀態將會很不方便。如果真的將build方法放在StatefulWidget中的話,由于構建用戶界面過程需要依賴State,所以build方法必須要添加一個State參數,大概是下面這樣:
Widget build(BuildContext context, State state){
//state.counter
...
}
這樣的話就只能將State的所有狀態聲明為公開的狀態,這樣才能在State類外部訪問狀態。但是,將狀態設置為公開后,狀態將不再具有私密性,這就會導致對狀態的修改將會變得不可控。但如果將build()方法放在State中的話,構建過程不僅可以直接訪問狀態,而且也無需公開私有狀態,這會非常方便。
- 繼承StatefulWidget不便
例如,Flutter中有一個動畫widget的基類 AnimateWidget,它繼承自StatefulWidget類。AnimatedWidget中引入了一個抽象方法 build(BuildContext context),繼承自AnimateWidget的動畫widget都要實現這個build方法?,F在設想一下,如果Statefulwidget類中已經有了一個build方法,正如上面所述,此時build方法需要接收一個state對象,這就意味著AnimateWidget必須將自己的State對象(記為_animatedWidgetState)提供給子類,因為子類需要在其build方法中調用父類build方法,代碼可能如下:
class MyAnimationWidget extends AnimatedWidget{
@override
Widget build(BuildContext context, State state){
//由于子類要用到AnimatedWidget的狀態對象_animatedWidgetState,
//所以AnimatedWidget必須通過某種方式將其狀態對象_animatedWidgetState
//暴露給其子類
super.build(context, _animatedWidgetState)
}
}
這樣很顯然是不合理的,因為:
1、AimatedWiget的狀態對象是AnimatedWidget內部實現細節,不應該暴露給外部。
2、如果要將父類狀態暴露給子類,那么必須得有一種傳遞機制,而做這一套傳遞機制是無意義的,因為父子類之間狀態的傳遞和子類本身邏輯是無關的。
綜上所述,可以發現,對于StatefulWidget,將build方法放在State中,可以給開發帶來很大的靈活性。
在Widget樹中獲取State對象
由于StatefulWidget的具體邏輯都在其State中,所以很多時候,我們需要獲取StatefulWidget對應的State對象來調用一些方法,比如Scaffold組件對應的狀態類ScaffoldState中就定義打開SnackBar(路由頁底部提示條)的方法,我們有兩種方法在子widget樹中獲取父級StatefulWidget的State對象。
- 通過Context獲取
context對象有一個 ancestorStateOfType(TypeMatcher) 方法,該方法可以從當前節點沿著widget樹向上查找指定類型的StatefulWidget對應的State對象。下面是實現打開SnackBar的實例:
Scaffold(
appBar: AppBar(
title: Text("子樹中獲取State對象"),
),
body: Center(
child: Builder(builder: (context) {
return RaisedButton(
onPressed: () {
// 查找父級最近的Scaffold對應的ScaffoldState對象
ScaffoldState _state = context.ancestorStateOfType(
TypeMatcher<ScaffoldState>());
//調用ScaffoldState的showSnackBar來彈出SnackBar
_state.showSnackBar(
SnackBar(
content: Text("我是SnackBar"),
),
);
},
child: Text("顯示SnackBar"),
);
}),
),
);
一般來說,如果StatefulWidget的狀態是私有的(不應該向外部暴露),那么我們代碼中就不應該去直接獲取其State對象;如果StatefulWidget的狀態是希望暴露出來的(通常還有一些組件的操作方法),我們則可以去直接獲取其State對象。但是通過 context.ancestorStateOfType 獲取 StatefulWidget的狀態方法是通用的,我們并不能在語法層面指定StatefulWidget的狀態是否私有,所以在Flutter開發中便有了一個默認的約定:如果StatefulWidget的狀態是希望暴露出來的,應該在StatefulWidget中提供一個of靜態方法來獲取其State對象,開發者便可直接通過該方法來獲?。蝗绻鸖tate不希望暴露,則不提供of方法。這個約定在Flutter SDK里隨處可見。所以,上面示例中的Scaffold也提供了一個of方法,我們其實可以直接調用它:
...//省略無關代碼
// 直接通過of靜態方法來獲取ScaffoldState
ScaffoldState _state = Scaffold.of(context);
_state.showSnackBar(
SnackBar(
content: Text("我是SnackBar"),
),
);
- 通過GlobalKey
Flutter還有一種通用的獲取State對象的方法:通過GlobalKey來獲取,分為兩步:
1、給目標StatefulWidget添加GlobalKey。
// 定義一個globalKey,由于GlobalKey要保持全局唯一性,我們使用靜態變量存儲。
static GlobalKey<ScaffoldState> _globalKey = GlobalKey();
...
Scaffold(
key: _globalKey, // 設置key
...
)
2、通過GlobalKey獲取State對象
_globalKey.currentState.openDrawer()
GlobalKey是Flutter提供的一種在整個App中引用element的機制。如果一個widget設置了GlobalKey,那么我們便可以通過globalKey.currentWidget獲取該widget對象,globalKey.currentElement來獲得widget對應的element對象,如果當前widget是StatefulWidget,則可以通過globalKey.currentState來獲得該widget對應的state對象。
注意:使用GlobalKey開銷比較大,如果有其他可選方案,應該避免使用它。另外,同一個GlobalKey在整個widget樹中必須是唯一的,不能重復。