Flutter中的Widget
Widget是Flutter框架裡核心類。在Flutter裡最核心的是用widgets構建UI介面。widgets描述的UI介面的長相及當前的配置與狀態。當widgets的狀態改變後,widgets將重構它的描述,框架會與前一個描述做比對,對渲染樹從前一個狀態到當前的狀態做出最小的改變。
在Native開發中,View就代表一個渲染類,是要最終由渲染管線渲染到螢幕上去的,所以比較重。而在Flutter當中Widget是用來描述UI的不可變資料結構。初學者很容易會把它當作一個View來使用,會不自覺得持有並複用它(主要還是Native的思維造成,以為Widget和View一樣比較重)。事實上Widget就一資料結構,建立與銷燬都比較輕量(尤其Dart語言專門為它優化過),所以儘量根據資料狀態生成Widget即可(當然在需要考慮效能的地方,可以快取或者使用const Widget)。
用Flutter開發介面,要理解Flutter的響應式開發。概括來說就是當UI改變時,我們給出一個此刻UI的快照(即Widget Tree),Flutter引擎拿到該快照會自動與前一刻的快照作比對,需要建立的就建立,可以複用的複用,能刪除的就刪除,最終自動渲染出UI。
所以在Flutter開發中,我們不太關心UI當中的區域性重新整理調整的細節(比如add, remove update等),關心的是任一時刻與資料狀態對應的整體UI快照。
這種整體的思維更符合人的思維,只是因為之前的UI開發框架不夠聰明,導致我們一直採用指令式程式設計,一時不習慣而已,適應了Flutter的思考模式,開發效率一定會有很大的提升。
這篇教程只涉及Flutter中的Widget,對Widget作詳細的解釋。
原始碼基於Flutter1.7.8。
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
繼承關係:
Widget
: DiagnosticableTree
: Diagnosticable
Diagnosticable及DiagnosticableTree都是很輕量的,提供了一些診斷及除錯方法,所以從原始碼可以看出Widget是很輕量的,它僅僅是對UI中的一小部分的描述或者配置。所以Widget是輕量的,短暫的。框架會呼叫createElement()
生成Element,Element則是比較持久的,可變的。Element管理渲染樹。
Widget被註釋為immutable
即不可變的,所以Widget沒有任何可變的狀態,所有的欄位都應該是final
。
傳統的View在View Tree中只能出現一次,而Widget可以在Widget Tree出現多次,0到多次。同一個Widget可以在Widget Tree被複用多次,但每次都會生成與Widget相對應的不同的Element。
原始碼中只提供了key
這個欄位,Flutter當中key用來控制Element樹當中新舊Widget的替換。Flutter通過canUpdate
判斷,如果兩個Widget的runtimeType
及key
都是相等的,其所對應的Element就會用新的Widget替換舊的Widget(通過呼叫Element.update
,傳遞新的Widget),否則,舊的Element要從樹中移除,新的Element要新增到樹當中。
普通的key只能使同一個位置的Element達到複用效果,開發當中肯定有希望跨不同位置Element複用的情形(比如該Element重建很重,或者需要該位置Element能保持狀態)。GlobalKey
則可以允許element在樹當中移動時(改變父element)不丟失狀態。當一個新的WidgetB,所對應位置的element的舊的widgetA與它不匹配(key及type都不符合),但是在前一幀有一個WidgetC的global key與它相同,則WidgetC對應的element會移到widgetB的位置,從而達到複用。
Widget是基類,一個典型的樹型結構當中,需要有葉子結點,需要有容器結點,也許還需要有一些特殊結點(比如用來跨結點傳遞資料)。Flutter提供的容器類Widget有StatelessWidget
及StatefullWidget
,這兩者可以管理一組Widget,還提供了InheritedWidget
用於跨節點資料傳遞,而像Image
等則是葉子結點。
StatelessWidget
StatelessWidget
是無狀態的Widget,原始碼如下:
abstract class StatelessWidget extends Widget {
const StatelessWidget({ Key key }) : super(key: key);
@override
StatelessElement createElement() => StatelessElement(this);
@protected
Widget build(BuildContext context);
}
複製程式碼
相比Widget
,StatelessWidget
提供了build
方法,通過build
方法構建一組Widget來更具體地描述UI的一部分。整個Widget樹的構建過程是遞迴的,直到生成一棵完整具體的UI描述樹。
框架會用build方法生成的一組widgets替換(更新或者刪除)該widget自己的子樹,
StatelessWidget應該在什麼場景下使用呢?該部分UI的描述完全可以由該Widget自己的配置資訊及構建上下文(BuildContext
,即該Widget自己對應的Element)描述,不依賴其它任何外部資訊。這也意味著該Widget是無狀態的。而需要動態改變的,比如改變需要內部的一個時鐘驅動,又或者改變依賴於系統的狀態,這時就需要考慮使用有狀態的StatefullWidget
。
build
方法的呼叫有三種時機
StatelessWidget
第一次插入到樹中時(其所對應的Element首次插入到Element樹中時)- 父物件改變了
StatelessWidget
的配置 - 依賴的
InheritedWidget
發生改變
出於效能的考慮,需要減少build方法的頻繁呼叫及提高build方法的效率。
如果父物件是有規律地改變StatelessWidget
的配置,或者依賴的InheritedWidget
頻繁地變動,為了能有一個平滑的渲染效能,需要優化build的效能。
為了減少重建無狀態Widget對效能的衝擊,有以下技術可以採用:
- 減少層級,不僅要最小化build方案構建的Widget層級,這些被構建的Widget自身的層級也要最小化。比如一個子節點需要特別設定的方式去定位,應該使用
Align
或者CustomSingleChildLayout
,避免使用這些複雜的排列如Row
,Column
,Padding
,SizedBox
等。如果繪製圖形效果需要多個Container
及Decoration
組成複雜的分層效果,那應該考慮使用單個CustomPaint
Widget。 - 儘可能使用
const
widget,並且提供一個const
的widget建構函式。const
宣告的值在編譯時其值是確定的,如果值相同,const會引用相同值,避免重複建立。 - 考慮使用
StatefullWidget
重構。這樣可以使用StatefullWidget
的一些優化技術,比如快取子樹或者使用GlobalKey
當樹的結構變化時。 - 分拆成多個Widget。如果頻繁的重構是由
InheritedWidget
導致的,可以考慮拆分成多個Widget,將需要改變的部分放到樹的葉子節點中。
StatefulWidget
StatefulWidget
是一個有可變狀態的Widget
先看原始碼
abstract class StatefulWidget extends Widget {
const StatefulWidget({ Key key }) : super(key: key);
@override
StatefulElement createElement() => StatefulElement(this);
@protected
State createState();
}
複製程式碼
State
用於StatefulWdiget
的內部狀態及邏輯。
State是應用程式中一段資訊,當widget需要構建時可以同步讀取該資訊,在widget的生命週期裡,State會發生變動。當State變動後,需要確保widget可以馬上收到通知,呼叫State.setState
。
與StatelessWidget
相同,StatefulWidget
也是通過建立一組widget來描述UI中的一部分,它的構建過程也是遞迴的,直到UI的具體的描述能完全生成。不同的是StatelessWidget
通過build
方法生成這組widget,而StatefulWidget
則通過State.build
來生成。
當UI描述的一部分是動態改變時,就可以使用StatefulWidget
。比如一個內部時鐘驅動的狀態或者依賴於系統的狀態。
前面說過widget都是不可變的,所以StatefulWidget
本身是不可變的,可變的狀態都儲存在State
物件,系統通過呼叫createState
建立獨立的State。可變的狀態也可能在State
訂閱的物件中,比如SatefulWidget
本身的一引起欄位引用Stream
或者ChangeNotifier
。
當需要建立StatefulWidget
對應的Element物件時,框架會呼叫createState
建立State
。如果同一個StatefulWidget
被多次插入到widget樹中,State
也會被建立多份。周樣的,如果StatefulWidget
從樹中移除隨後又再次插入到樹中,框架會重新呼叫createState
生成一份新的State,這樣可以簡化State
的生命週期。
StatefulWidget
從一個位置移到另一個位置,顯示框架會重新呼叫createState
建立新的State
,這樣狀態會丟失。怎麼樣可以做到複用狀態呢?答案就是GlobalKey
,使用了GlobalKey
,會複用保持該State
,從而不丟失狀態。具有GlobalKey
的widget在樹中最多隻有一個位置可使用,最多隻有一個與之關聯的Element
。當具有GlobalKey
的widget從一個位置移到另一個位置時,框架可以利用該有利條件,將舊位置上widget的子樹嫁接到新位置widget的子樹上,而不是直接重構新位置上widget的子樹,這樣,State
也跟著從舊位置嫁接複用到新位置上。儘管如此,為了能順利實現複用,新舊位置的移動必須是在同一動畫幀中。
StatefulWidget
有兩種主要使用型別。
第一種是隻在[State.initState]分配資源並且在[State.dispose]中銷燬資源,不依賴於InheritedWidget
,並且不會呼叫State.setState
。這種型別一般用於程式或者頁面的根節點,通過ChangeNotifier
或者Stream
等與子widgets互動。這種模式代價比較低(在CPU且GPU週期方面),因為它只要構建一次,再也不會重新整理並重構。它們一般有一引起比較複雜且深度的build方法。
每二種widget會依賴InheritedWidget
或者會呼叫State.setState
(可能同樣會合用State.initState
或者State.didChangeDependences
)。在整個應用生命週期當中它們會被多資重構,所以基於效能的考慮,需要將重構的影響最小化。
為了減少重構的影響,有以下幾種技術可以採用:
- 將State推到葉子結點上。比如,如果一個頁面是時鐘驅動的,建立一個專用的時鐘Widget只更新它自己,而不是將該
State
放到頁面的頂部,當收到時鐘資訊號時整個頁面將要重構。 - 減少build方法產生的widgets層級
- 如果子樹沒有改變,快取代表該子樹的widget並每次複用
- 儘可能使用
const
widgets。(如前面解釋過,這樣會快取widgets並複用它) - 避免改變子樹的深度或者改變子樹中widget的型別。比如,相比返回子結點或者返回用
IgnorePointer
包裝的子結點,最好一直返回用IgnorePointer
包裝的子結點,並控制IgnorePointer.ignoring
的屬性。這是因為改變子樹的深度將導致整棵子樹的重建,重新佈局及重新繪製。而改變屬性僅僅要求渲染樹很小的改動(這個例子中,不會有重新佈局,不會有重新繪製) - 如果深度因為一些原因必須要改變,可考慮將子樹中的公共部分用具有
GlobalKey
的Widget包裝起來,該GlobalKey
在StatefulWidget
生命週期裡保持一致。(如果其它widget的key不方便賦值,KeyedSubtree
就可以派上用場了)
State
@optionalTypeArgs
abstract class State<T extends StatefulWidget> extends Diagnosticable {
T get widget => _widget;
T _widget;
_StateLifecycle _debugLifecycleState = _StateLifecycle.created;
bool _debugTypesAreRight(Widget widget) => widget is T;
BuildContext get context => _element;
StatefulElement _element;
bool get mounted => _element != null;
@protected
@mustCallSuper
void initState() {
assert(_debugLifecycleState == _StateLifecycle.created);
}
@mustCallSuper
@protected
void didUpdateWidget(covariant T oldWidget) { }
@protected
@mustCallSuper
void reassemble() { }
@protected
void setState(VoidCallback fn) {
final dynamic result = fn() as dynamic;
_element.markNeedsBuild();
}
@protected
@mustCallSuper
void deactivate() { }
@protected
@mustCallSuper
void dispose() {
assert(_debugLifecycleState == _StateLifecycle.ready);
assert(() { _debugLifecycleState = _StateLifecycle.defunct; return true; }());
}
@protected
Widget build(BuildContext context);
@protected
@mustCallSuper
void didChangeDependencies() { }
}
複製程式碼
State的生命週期
digraph {
rankdir=TB;
createState [shape=box label="createState" fillcolor=lightblue style=filled];
BuildContext [shape=box label="關聯BuildContext及widget" fillcolor=lightblue style=filled];
initState [shape=box fillcolor=lightgray style=filled];
didChangeDependencies [shape=box fillcolor=lightgray style=filled];
build [shape=box fillcolor=lightgray style=filled];
renderTree [label="render tree"];
removeWidget [label="remove widget"];
didUpdateWidget [shape=box fillcolor=lightgray style=filled];
didChangeDependencies_ [shape=box label="didChangeDependencies" fillcolor=lightgray style=filled];
build_ [shape=box label="build" fillcolor=lightgray style=filled];
createState -> BuildContext -> initState -> didChangeDependencies -> build;
build -> renderTree;
setState [shape=box fillcolor=lightgray style=filled];
{rank=same; build; setState;}
setState -> build;
renderTree [shape=MRecord fillcolor=lightblue style=filled];
removeWidget [shape=rect fillcolor=lightblue style=filled];
reinsert [shape=rect style=rounded];
renderTree -> didUpdateWidget -> build_;
renderTree -> didChangeDependencies_ -> build_;
build_:w -> renderTree:w;
{rank=same; renderTree; removeWidget;}
renderTree:e -> removeWidget:w;
deactivate [shape=box fillcolor=lightgray style=filled];
dispose [shape=box fillcolor=lightgray style=filled];
removeWidget -> deactivate;
deactivate:s -> dispose;
deactivate -> reinsert;
reinsert -> build_;
dispose -> over;
over [shape=box fillcolor=lightblue style=filled];
reinsert [shape=box fillcolor=lightblue style=filled];
{rank=same; dispose; build_}
{rank=same; didUpdateWidget; didChangeDependencies_;}
}
複製程式碼
- 框架呼叫
StatefulWidget.createState
建立State
物件 - 新建立的
State
物件關聯到BuildContext
。這種關聯是永久的:State
物件永遠不會改變它的BuildContext
。儘管如此,BuildContext
可以帶著它的子樹移到樹的其它位置。這時,State
物件被認為是掛裁的mounted
。 - 框架呼叫
initState
。State
子類應該重寫initState
執行一次性初始化,初始化依賴於BuildContext
或者該widget。BuildContext
和該widget分別作為State
的context
及widget
的屬性,當initState
被呼叫後,它們是可用的。 - 框架呼叫
didChangeDependencies
。子類應該重寫didChangeDependencies
執行涉及InheritedWidget
的初始化。如果BuildContext.inheritFromWidgetOfExactType
被呼叫了,隨後當inherited widgets發生改變或者該widget在樹中發生轉移,didChangeDependencies
方法被會被再次呼叫。 - 這時
State
物件已經完全初始化了。框架可能多次呼叫build
方法獲得該子樹對UI的描述。State
物件可以通過呼叫setState
自發地請求重構子樹,這標誌著子樹的內部狀態發生改變,並可能會影響到UI。 - 在這段時間,父widget可能重構並請求更新樹中的這個位置,該更新顯示一個相同
runtimeType
和Widget.key
的新widget。當這種情況發生後,框架會更新State.widget
屬性指向這個新的widget,並使用先前的widget呼叫didUpdateWidget
方法。State
物件應該重寫didUpdateWidget
響應與它關聯的widget的改變(比如開始隱式動畫)。呼叫完didUpdateWidget
後框架總是會呼叫build
方法,所在在didUpdateWidget
中呼叫setState
是多餘的。 - 在開發階段,當熱過載發生後,
reassemble
方法會被呼叫。這提供了重置資料的機會,這些資料是由initState
方法準備好的。 - 如果包令
State
物件的子樹從樹中移除(比如你widget構建了不周runtimeType
或者Widget.key
的子widget),框架將會呼叫deactivate
方法。子類應該重寫該方法清理State
物件與樹中其它element的引用關係(比如為祖先結點提供了指向後代RenderObject
的指標)。 - 此時,框架可能將子樹重新插入到樹的其它部分。如果這種情況發生,框架將確保呼叫
build
方法使State
物件有機會適配樹中的新位置。如果框架確實要重插入該子樹,框架將在動畫幀結束之前執行插入,這時子樹已經從樹中移除了。因此,State
物件可以延遲釋放資源,直到dispose
被呼叫後再釋放資源(從1.7.8原始碼可以看出,只有引用GlobalKey的widget有可能被重新插入)。 - 如果框架在當前動畫幀結束之前沒有重新插入該子樹,框架將會呼叫
dispose
,這意昧著State
物件永遠也不會重新構建。子類應該重寫該方法釋放持有的資源(比如結束作何活動的動畫)。 - 在框架呼叫
dispose
後,State
物件是被解除安裝的,並且mounted
屬性是false。這時如果再呼叫setState
是錯誤的(這一步要特別注意,實際開發中,大部分異常都與此有關,當非同步呼叫返回後,通常需要改變狀態,此時一定要判斷一下State
物件的狀態,否則會丟擲異常)。這時生命週期走到了最終:當State
物件被銷燬後沒有任何辦法可以重新掛載。
從框架的原始碼分析:
class StatefulElement extends ComponentElement {
StatefulElement(StatefulWidget widget)
: _state = widget.createState(),
super(widget) {
_state._element = this;
_state._widget = widget;
_StateLifecycle.created);
}
}
複製程式碼
從StatefulElement原始碼可以看出,建立StatefulElement時,一併會呼叫StatefulWidget.createState
建立State
物件,並關聯Buildcontext
及widget。
_state._element = this;
該行程式碼關聯BuildContext
,並且此關聯關係是不變的,在State物件的生命週期中,BuildContext
是再不可變的,Flutter當中,BuildContext
是由Element來現的。
_state._widget = widget;
關聯widget。
@override
void mount(Element parent, dynamic newSlot) {
super.mount(parent, newSlot);
assert(_child == null);
assert(_active);
_firstBuild();
assert(_child != null);
}
@override
void _firstBuild() {
...
try {
_debugSetAllowIgnoredCallsToMarkNeedsBuild(true);
final dynamic debugCheckForReturnedFuture = _state.initState() as dynamic;
} finally {
_debugSetAllowIgnoredCallsToMarkNeedsBuild(false);
}
_state.didChangeDependencies();
super._firstBuild();
}
複製程式碼
當Element通過mount
方法被插入到Elemnt樹中,會呼叫_firstBuild
,_firstBuild
則依次呼叫了State.initState
及State.didChangeDependencies
。
@override
Widget build() => widget.build(this);
void _firstBuild() {
rebuild();
}
void rebuild() {
if (!_active || !_dirty)
return;
...
performRebuild();
...
}
@override
void performRebuild() {
...
try {
built = build();
debugWidgetBuilderValue(widget, built);
} catch (e, stack) {
built = ErrorWidget.builder(_debugReportException('building $this', e, stack));
} finally {
_dirty = false;
}
...
複製程式碼
_firstBuild
緊接著會呼叫rebuild
,rebuild
呼叫performRebuild
,performRebuild
呼叫build
,build
方法則會呼叫State.build
建立子樹。
可見,State.createState
,關聯BuildContext
及widget
,State.initState
,State.didChangeDependencies
,State.build
,在StatefulElement插入到樹中時,是一氣呵成依次呼叫的。
@protected
void setState(VoidCallback fn) {
final dynamic result = fn() as dynamic;
_element.markNeedsBuild();
}
複製程式碼
State.setState
會馬上同步執行傳入的回撥,並標記element需要重建。隨後element會呼叫State.build
更新子樹。
@override
void update(StatefulWidget newWidget) {
super.update(newWidget);
assert(widget == newWidget);
final StatefulWidget oldWidget = _state._widget;
_dirty = true;
_state._widget = widget;
try {
final dynamic debugCheckForReturnedFuture = _state.didUpdateWidget(oldWidget) as dynamic;
} finally {
_debugSetAllowIgnoredCallsToMarkNeedsBuild(false);
}
rebuild();
}
複製程式碼
當新舊widget的runtimeType
及key
是相等時,框架會呼叫Element.update
,該方法會呼叫StatefulWidget.didUpdateWidget
通知widget已經更新。
@override
void didChangeDependencies() {
super.didChangeDependencies();
_state.didChangeDependencies();
}
複製程式碼
當StatefulWidget
依賴的InheritedWidget
改變後,Element收到didChangeDependencies
時,會通知State
,呼叫State.didChangeDependencies
。
同理,element的deactivate
及dispose
方法都會通知到StatefulWidget
。
ProxyWidget
持有一個子widget。其它只有一個子widget類的基類
原始碼
abstract class ProxyWidget extends Widget {
const ProxyWidget({ Key key, @required this.child }) : super(key: key);
final Widget child;
}
複製程式碼
InheritedWidget
可以高效傳遞資料到後代結點。
使用BuildContext.inheritFromWidgetOfExactType
可以取得最近的特定型別的inherit widget例項。
這也會導致呼叫它的widget在inherited widget發生改變後重構。
原始碼:
abstract class InheritedWidget extends ProxyWidget {
const InheritedWidget({ Key key, Widget child })
: super(key: key, child: child);
@override
InheritedElement createElement() => InheritedElement(this);
@protected
bool updateShouldNotify(covariant InheritedWidget oldWidget);
}
複製程式碼
使用例子
class FrogColor extends InheritedWidget {
const FrogColor({
Key key,
@required this.color,
@required Widget child,
}) : assert(color != null),
assert(child != null),
super(key: key, child: child);
final Color color;
static FrogColor of(BuildContext context) {
return context.inheritFromWidgetOfExactType(FrogColor) as FrogColor;
}
@override
bool updateShouldNotify(FrogColor old) => color != old.color;
}
複製程式碼
按照慣例InheritedWidget
會提供一個靜態的of
方法,該方法會呼叫BuildContext.inheritFromWidgetOfExactType
。在域中如果沒有這樣的widget,這將允許類定義自己的返回邏輯。在上面的例子中,返回值有可能為空,在這種情況下,可以返回一個預設值。
of
返回的資料也可以不是inherited widget,上面這個例子中,返回的是Color
。
有時,inherited widget是一個實現了其它類的細節,所以是私有的。of
方法將由其它公開類提供。比如Theme
實現了StatelessWidget
構建私有的inherited widget;Theme.of
方法通過BuildContext.inheritFromWidgetOfExactType
尋找inherited widget並且返回。
當InheritedWidget
重建時,需要通知遺傳此widget的其它widget重建,但有時並不需要通知。比如該widget持有的資料與舊的widget持有的相同,我們沒有必要通知遺傳widget重建。
框架通過呼叫InheritedWidget.updateShouldNotify
來區別這種情況,呼叫該方法會傳遞舊的widget作為引數。舊的widget的runtimeType
保證與該類相同。