在前面的章節介紹中,咱們知道Flutter中幾乎全部的對象都是一個Widget(組件),與原生開發中的「控件」不一樣的是,Flutter中的Widget的概念更普遍,它不只能夠表明UI元素,也能夠表明一些功能性的組件,如:用於手勢檢測的GestureDetector
組件、用於應用主題數據傳遞的Theme
組件等等,而原生開發中的「控件」一般只是指UI元素。在後面的內容中,咱們在描述UI元素時,可能會用到「控件」、「組件」這樣的概念,讀者內心須要知道它們就是Widget,只是針對不一樣場景所作的不一樣表述而已。因爲Flutter主要就是用於構建用戶界面的,因此在大多數時候,讀者能夠認爲Widget就是一個「控件」,沒必要糾結於概念。編程
在Flutter中,Widget的功能是「描述一個UI元素的配置數據」,也就是說,Widget其實並非表示最終繪製在設備屏幕上的顯示元素,而只是顯示元素的一個配置數據。實際上,在Flutter中真正表明屏幕上顯示元素的類是Element
,而Widget只是描述Element
的一個配置。有關Element
的詳細介紹將在後續進行,讀者如今只須要知道,Widget只是UI元素的一個配置數據,而且一個Widget能夠對應多個Element
,這是由於同一個Widget對象能夠被添加到UI樹的不一樣部分,而真正渲染時,UI樹的每個Element
節點都會對應一個Widget對象。bash
總結一下:框架
Element
的配置數據,Widget樹其實是一個配置樹,而真正的UI渲染樹是由Element
構成的;但因爲Element
是經過Widget生成的,因此它們之間又有對應關係,所以在大多數場景中,咱們能夠寬泛地認爲Widget樹就是指UI控件樹或UI渲染樹。Element
對象。很好理解,根據同一份配置(Widget),能夠建立多個實例(Element)。@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
即「診斷樹」,主要做用是提供調試信息。Key
:這個Key
屬性相似於React/Vue(二者都是支持響應式編程的Web開發框架)中的Key
,主要做用是決定是否在下一次build
時複用舊的Widget,決定的條件在canUpdate()
方法中。createElement()
:正如上述所說「一個Widget能夠對應多個Element
」;Flutter Framework在構建UI樹時,會先調用此方法生成對應節點的Element
對象。此方法是Flutter Framework隱式調用的,在咱們開發過程當中基本不會調用到。debugFillProperties(...)
覆寫了父類的方法,主要是設置診斷樹的一些特性。canUpdate(...)
是一個靜態方法,它主要用於在Widget樹從新build
時複用舊的Widget。其實具體來講,應該是:是否用新的Widget對象去更新舊UI樹上所對應的Element
對象的配置。經過其源碼能夠看出,只要newWidget
與oldWidget
的runtimeType
和key
同時相等時纔會用newWidget
去更新Element
對象的配置,不然就會建立新的Element
。有關Key和Widget複用的細節將在後續進行,讀者如今只須要知道,若是爲Widget顯式添加Key
的話可能(但不必定)會使UI在從新構建時變得高效,讀者目前能夠先忽略此參數。在後面的示例中,咱們只在構建列表項UI時會顯式指定key
。less
另外Widget
類自己是一個抽象類,其中最核心的就是定義了createElement()
方法,在Flutter開發中,咱們通常都不用直接繼承Widget
類來實現Widget,而是會經過繼承StatelessWidget
和StatefulWidget
來間接繼承Widget
類從而實現Widget,而StatelessWidget
和StatefulWidget
都是直接繼承自Widget
類,而這兩個類也正是Flutter中很是重要的兩個抽象類,它們引入了兩種Widget模型,接下來咱們將重點介紹一下這兩個類。ide
在以前咱們有一篇《初略講解Flutter應用模板源碼:計數器示例》的文章,在那裏面咱們已經簡單介紹過StatelessWidget
和StatefulWidget
,而StatelessWidget
相對於StatefulWidget
來講比較簡單,它繼承自Widget
,覆寫了createElement()
方法:函數
@override
StatelessElement createElement() => new StatelessElement(this);
複製代碼
StatelessElement
間接繼承自Element
類,與StatelessWidget
相對應(StatelessWidget
做爲StatelessElement
的配置數據)。源碼分析
StatelessWidget
用於不須要維護狀態的場景,它一般在build
方法中經過嵌套其它Widget來構建UI,在構建過程當中會以遞歸的方式構建其嵌套的Widget。舉個例子:post
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),
),
);
}
}
複製代碼
上面的代碼,實現了一個回顯字符串的Echo
Widget。ui
按照慣例,Widget的構造函數應使用命名參數,命名參數中的必要參數要添加
@required
標註,這樣有利於靜態代碼分析器進行檢查;另外,在繼承Widget
時,第一個參數一般應該是Key
,若是接受子Widget
的child
參數,那麼一般應該將它放在參數列表的最後;最後,Widget中的屬性應被聲明爲final
(如:text
和backgroundColor
),防止被意外改變。this
而後咱們能夠經過以下方式使用它:
Widget build(BuildContext context) {
return Echo(text: "hello world");
}
複製代碼
和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
相對應(StatefulWidget
做爲StatefulElement
的配置數據)。StatefulElement
中可能會屢次調用createState()
來建立State
(狀態)對象。createState()
用於建立和Stateful Widget相關的狀態,它在Stateful Widget的生命週期中可能會被屢次調用。例如,當一個Stateful Widget同時插入到Widget樹的多個位置時,Flutter Framework就會調用該方法爲每個位置生成一個獨立的State
實例,其實,本質上就是一個StatefulElement
對應一個State
實例。在本章中常常會出現「樹」的概念,在不一樣的場景可能指不一樣的意思,好比在說「Widget樹」時它能夠指Widget結構樹,但因爲Widget與Element有對應關係(一可能對多),所以在有些場景(Flutter的SDK文檔中)也代指「UI樹」的意思。而在Stateful Widget中,
State
對象也和StatefulElement
具備對應關係(一對一),因此在Flutter的SDK文檔中,能夠常常看到「從樹中移除State
對象」或「插入State
對象到樹中」這樣的描述。其實,不管哪一種描述,其意思都是在描述「一棵構成用戶界面的節點元素的樹」,所以,在本章以及後續文章當中出現的各類「樹」,若是沒有特別說明,讀者均可抽象的認爲它是「一棵構成用戶界面的節點元素的樹」。
一個StatefulWidget
類會對應一個State
類,State
表示與其對應的StatefulWidget
要維護的狀態,State
中保存的狀態信息能夠:
build
時能夠被同步讀取;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
,它是BuildContext
類的一個實例,表示構建Widget的上下文,它是操做Widget在樹中位置的一個句柄,它包含了一些查找、遍歷當前Widget樹中的一些方法;每個Widget
都有一個對應的context
對象。理解State的生命週期對Flutter開發很是重要,爲了加深讀者印象,下面咱們經過一個實例來演示一下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
方法。
而後咱們點擊⚡️按鈕或按「r」鍵進行熱重載,控制檯輸出日誌以下:
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");
}
複製代碼
而後咱們點擊⚡️按鈕或按「r」鍵進行熱重載,控制檯輸出日誌以下:
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()
中調用BuildContext.inheritFromWidgetOfExactType
方法。didChangeDependencies()
:當State對象的依賴發生變化時會被調用;例如:在以前的build()
中包含了一個InheritFromWidget
,而後在以後的build()
中InheritFromWidget
發生了變化,那麼此時InheritFromWidget
的子Widget的didChangeDependencies()
回調函數都會被調用。典型的場景是當系統語言Locale或應用主題改變時,Flutter Framework會通知Widget調用此回調函數。build()
:它主要是用於構建Widget子樹的,會在以下場景被調用:
initState()
以後;didUpdateWidget()
以後;setState()
以後;didChangeDependencies()
以後;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
覆寫其方法時,對於包含@mustCallSuper
標註的父類方法,都要在子類方法中先調用父類方法。