Flutter Widget簡介

首先,在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生命周期如圖所示:


屏幕快照 2019-09-18 下午5.12.05.png

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樹中必須是唯一的,不能重復。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,882評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,208評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,746評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,666評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,477評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,960評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,047評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,200評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,726評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,617評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,807評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,327評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,049評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,425評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,674評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,432評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,769評論 2 372

推薦閱讀更多精彩內容