Widget
概念
在前面的章節介紹中,我們知道Flutter中幾乎所有的物件都是一個Widget(元件),與原生開發中的“控制元件”不同的是,Flutter中的Widget的概念更廣泛,它不僅可以代表UI元素,也可以代表一些功能性的元件,如:用於手勢檢測的GestureDetector
元件、用於應用主題資料傳遞的Theme
元件等等,而原生開發中的“控制元件”通常只是指UI元素。在後面的內容中,我們在描述UI元素時,可能會用到“控制元件”、“元件”這樣的概念,讀者心裡需要知道它們就是Widget,只是針對不同場景所做的不同表述而已。由於Flutter主要就是用於構建使用者介面的,所以在大多數時候,讀者可以認為Widget就是一個“控制元件”,不必糾結於概念。
Widget與Element
在Flutter中,Widget的功能是“描述一個UI元素的配置資料”,也就是說,Widget其實並不是表示最終繪製在裝置螢幕上的顯示元素,而只是顯示元素的一個配置資料。實際上,在Flutter中真正代表螢幕上顯示元素的類是Element
,而Widget只是描述Element
的一個配置。有關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原始碼分析
@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
。
另外Widget
類本身是一個抽象類,其中最核心的就是定義了createElement()
方法,在Flutter開發中,我們一般都不用直接繼承Widget
類來實現Widget,而是會通過繼承StatelessWidget
和StatefulWidget
來間接繼承Widget
類從而實現Widget,而StatelessWidget
和StatefulWidget
都是直接繼承自Widget
類,而這兩個類也正是Flutter中非常重要的兩個抽象類,它們引入了兩種Widget模型,接下來我們將重點介紹一下這兩個類。
StatelessWidget
在之前我們有一篇《初略講解Flutter應用模板原始碼:計數器示例》的文章,在那裡面我們已經簡單介紹過StatelessWidget
和StatefulWidget
,而StatelessWidget
相對於StatefulWidget
來說比較簡單,它繼承自Widget
,覆寫了createElement()
方法:
@override
StatelessElement createElement() => new StatelessElement(this);
複製程式碼
StatelessElement
間接繼承自Element
類,與StatelessWidget
相對應(StatelessWidget
作為StatelessElement
的配置資料)。
StatelessWidget
用於不需要維護狀態的場景,它通常在build
方法中通過巢狀其它Widget來構建UI,在構建過程中會以遞迴的方式構建其巢狀的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),
),
);
}
}
複製程式碼
上面的程式碼,實現了一個回顯字串的Echo
Widget。
按照慣例,Widget的建構函式應使用命名引數,命名引數中的必要引數要新增
@required
標註,這樣有利於靜態程式碼分析器進行檢查;另外,在繼承Widget
時,第一個引數通常應該是Key
,如果接受子Widget
的child
引數,那麼通常應該將它放在引數列表的最後;最後,Widget中的屬性應被宣告為final
(如:text
和backgroundColor
),防止被意外改變。
然後我們可以通過如下方式使用它:
Widget build(BuildContext context) {
return Echo(text: "hello world");
}
複製程式碼
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
相對應(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
物件到樹中”這樣的描述。其實,無論哪種描述,其意思都是在描述“一棵構成使用者介面的節點元素的樹”,因此,在本章以及後續文章當中出現的各種“樹”,如果沒有特別說明,讀者都可抽象的認為它是“一棵構成使用者介面的節點元素的樹”。
State
一個StatefulWidget
類會對應一個State
類,State
表示與其對應的StatefulWidget
要維護的狀態,State
中儲存的狀態資訊可以:
- 在Widget
build
時可以被同步讀取; - 在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
,它是BuildContext
類的一個例項,表示構建Widget的上下文,它是操作Widget在樹中位置的一個控制程式碼,它包含了一些查詢、遍歷當前Widget樹中的一些方法;每一個Widget
都有一個對應的context
物件。
State生命週期
理解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()
之後; - 在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覆寫其方法時,對於包含@mustCallSuper
標註的父類方法,都要在子類方法中先呼叫父類方法。