Flutter中的Widget

阿輝_發表於2019-08-12

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的runtimeTypekey都是相等的,其所對應的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有StatelessWidgetStatefullWidget,這兩者可以管理一組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);

}
複製程式碼

相比WidgetStatelessWidget提供了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等。如果繪製圖形效果需要多個ContainerDecoration組成複雜的分層效果,那應該考慮使用單個CustomPaintWidget。
  • 儘可能使用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包裝起來,該GlobalKeyStatefulWidget生命週期裡保持一致。(如果其它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的生命週期

Flutter中的Widget

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
  • 框架呼叫initStateState子類應該重寫initState執行一次性初始化,初始化依賴於BuildContext或者該widget。BuildContext和該widget分別作為Statecontextwidget的屬性,當initState被呼叫後,它們是可用的。
  • 框架呼叫didChangeDependencies。子類應該重寫didChangeDependencies執行涉及InheritedWidget的初始化。如果BuildContext.inheritFromWidgetOfExactType被呼叫了,隨後當inherited widgets發生改變或者該widget在樹中發生轉移,didChangeDependencies方法被會被再次呼叫。
  • 這時State物件已經完全初始化了。框架可能多次呼叫build方法獲得該子樹對UI的描述。State物件可以通過呼叫setState自發地請求重構子樹,這標誌著子樹的內部狀態發生改變,並可能會影響到UI。
  • 在這段時間,父widget可能重構並請求更新樹中的這個位置,該更新顯示一個相同runtimeTypeWidget.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.initStateState.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緊接著會呼叫rebuildrebuild呼叫performRebuildperformRebuild呼叫buildbuild方法則會呼叫State.build建立子樹。

可見,State.createState,關聯BuildContextwidgetState.initStateState.didChangeDependenciesState.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的runtimeTypekey是相等時,框架會呼叫Element.update,該方法會呼叫StatefulWidget.didUpdateWidget通知widget已經更新。

@override
void didChangeDependencies() {
    super.didChangeDependencies();
    _state.didChangeDependencies();
}
複製程式碼

StatefulWidget依賴的InheritedWidget改變後,Element收到didChangeDependencies時,會通知State,呼叫State.didChangeDependencies

同理,element的deactivatedispose方法都會通知到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保證與該類相同。

相關文章