初略講解基礎Widgets之Widget、StatelessWidget和StatefulWidget

zane發表於2019-07-11

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類繼承自DiagnosticableTreeDiagnosticableTree即“診斷樹”,主要作用是提供除錯資訊。
  • 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物件的配置。通過其原始碼可以看出,只要newWidgetoldWidgetruntimeTypekey同時相等時才會用newWidget去更新Element物件的配置,否則就會建立新的Element

有關Key和Widget複用的細節將在後續進行,讀者現在只需要知道,如果為Widget顯式新增Key的話可能(但不一定)會使UI在重新構建時變得高效,讀者目前可以先忽略此引數。在後面的示例中,我們只在構建列表項UI時會顯式指定key

另外Widget類本身是一個抽象類,其中最核心的就是定義了createElement()方法,在Flutter開發中,我們一般都不用直接繼承Widget類來實現Widget,而是會通過繼承StatelessWidgetStatefulWidget來間接繼承Widget類從而實現Widget,而StatelessWidgetStatefulWidget都是直接繼承自Widget類,而這兩個類也正是Flutter中非常重要的兩個抽象類,它們引入了兩種Widget模型,接下來我們將重點介紹一下這兩個類。

StatelessWidget

在之前我們有一篇《初略講解Flutter應用模板原始碼:計數器示例》的文章,在那裡面我們已經簡單介紹過StatelessWidgetStatefulWidget,而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,如果接受子Widgetchild引數,那麼通常應該將它放在引數列表的最後;最後,Widget中的屬性應被宣告為final(如:textbackgroundColor),防止被意外改變。

然後我們可以通過如下方式使用它:

Widget build(BuildContext context) {
  return Echo(text: "hello world");
}
複製程式碼

初略講解基礎Widgets之Widget、StatelessWidget和StatefulWidget

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中儲存的狀態資訊可以:

  1. 在Widget build時可以被同步讀取;
  2. 在Widget生命週期中可以被改變;當State被改變時,可以手動呼叫其setState()方法通知Flutter Framework狀態發生改變,Flutter Framework在收到訊息後,會重新呼叫其build方法重新構建Widget樹,從而達到更新UI的目的。

State中有兩個常用屬性:

  1. widget,它表示與該State例項關聯的Widget例項,由Flutter Framework動態設定。注意,這種關聯並非永久的,因為在應用宣告週期中,UI樹上的某一個節點的Widget例項在重新構建時可能會變化,但State例項只會在第一次插入到樹中時被建立,當在重新構建時,如果Widget被修改了,Flutter Framework會動態設定State.widget為新的Widget例項。
  2. 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
複製程式碼

可以看到此時initStatedidChangeDependencies都沒有被呼叫,而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樹中移除時,deactivedispose會依次被呼叫。

下面我們來看看各個回撥函式:

  • 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的keyruntimeType同時相等時會返回true,也就是說在新舊Widget的keyruntimeType同時相等時didUpdateWidget()就會被呼叫。
  • deactivate():當State物件從樹中被移除時,會呼叫此回撥函式;在一些場景下,Flutter Framework會將State物件重新插到樹中,如包含此State物件的子樹在樹的一個位置移動到另一個位置時(可以通過GlobalKey來實現)會呼叫此回撥函式。如果移除後沒有重新插入到樹中則緊接著會呼叫dispose()方法。
  • dispose():當State物件從樹中被永久移除時呼叫;因此通常在此回撥函式中進行釋放資源等操作。
    StatefulWidgetLifecycle

注意: 在繼承StatefulWidget覆寫其方法時,對於包含@mustCallSuper標註的父類方法,都要在子類方法中先呼叫父類方法。

相關文章