《Flutter For Android學習日記》Widget簡介

概念

在前面的介紹中,我們知道在Flutter中幾乎所有的對象都是一個Widget。與原生開發(fā)中“控件”不同的是,F(xiàn)lutter中的Widget的概念更廣泛,它不僅可以表示UI元素,也可以表示一些功能性的組件如:用于手勢檢測GestureDetectorwidget、用于APP主題數(shù)據(jù)傳遞的Theme等等,而原生開發(fā)中的控件通常只是指UI元素。在后面的內容中,我們在描述UI元素時可能會用到“控件”、“組件”這樣的概念,讀者心里需要知道他們就是widget,只是在不同場景的不同表述而已。由于Flutter主要就是用于構建用戶界面的,所以,在大多數(shù)時候,讀者可以認為widget就是一個控件,不必糾結于概念。

Widget與Element

在Flutter中,Widget的功能是“描述一個UI元素的配置數(shù)據(jù)”,它就是說,Widget其實并不是表示最終繪制在設備屏幕上的顯示元素,而它只是描述顯示元素的一個配置數(shù)據(jù)。

實際上,F(xiàn)lutter中真正代表屏幕上顯示元素的類是Element,也就是說Widget只是描述Element的配置數(shù)據(jù)!Widget只是UI元素的一個配置數(shù)據(jù),并且一個Widget可以對應多個Element。這是因為同一個Widget對象可以被添加到UI樹的不同部分,而真正渲染時,UI樹的每一個Element節(jié)點都會對應一個Widget對象??偨Y一下:

Widget實際上就是Element的配置數(shù)據(jù),Widget樹實際上是一個配置樹,而真正的UI渲染樹是由Element構成;不過,由于Element是通過Widget生成的,所以它們之間有對應關系,在大多數(shù)場景,我們可以寬泛地認為Widget樹就是指UI控件樹或UI渲染樹。
一個Widget對象可以對應多個Element對象。這很好理解,根據(jù)同一份配置(Widget),可以創(chuàng)建多個實例(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中的key,主要的作用是決定是否在下一次build時復用舊的widget,決定的條件在canUpdate()方法中。
createElement():正如前文所述“一個Widget可以對應多個Element”;Flutter Framework在構建UI樹時,會先調用此方法生成對應節(jié)點的Element對象。此方法是Flutter Framework隱式調用的,在我們開發(fā)過程中基本不會調用到。
debugFillProperties(...) 復寫父類的方法,主要是設置診斷樹的一些特性。
canUpdate(...)是一個靜態(tài)方法,它主要用于在Widget樹重新build時復用舊的widget,其實具體來說,應該是:是否用新的Widget對象去更新舊UI樹上所對應的Element對象的配置;通過其源碼我們可以看到,只要newWidgetoldWidgetruntimeTypekey同時相等時就會用newWidget去更新Element對象的配置,否則就會創(chuàng)建新的Element
另外Widget類本身是一個抽象類,其中最核心的就是定義了createElement()接口,在Flutter開發(fā)中,我們一般都不用直接繼承Widget類來實現(xiàn)一個新組件,相反,我們通常會通過繼承StatelessWidgetStatefulWidget來間接繼承Widget類來實現(xiàn)。StatelessWidgetStatefulWidget都是直接繼承自Widget類,而這兩個類也正是Flutter中非常重要的兩個抽象類,它們引入了兩種Widget模型,接下來我們將重點介紹一下這兩個類。

StatelessWidget

我們已經簡單介紹過StatelessWidget,StatelessWidget相對比較簡單,它繼承自Widget類,重寫了createElement()方法:

@override
StatelessElement createElement() => new StatelessElement(this);

StatelessElement間接繼承自Element類,與StatelessWidget相對應(作為其配置數(shù)據(jù))。
StatelessWidget用于不需要維護狀態(tài)的場景,它通常在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),
      ),
    );
  }
}

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

Widget build(BuildContext context) {
  return Echo(text: "hello world");
}
運行結果

StatefulWidget

StatelessWidget一樣,StatefulWidget也是繼承自Widget類,并重寫了createElement()方法,不同的是返回的Element對象并不相同;另外StatefulWidget類中添加了一個新的接口createState()

abstract class StatefulWidget extends Widget {
  const StatefulWidget({ Key key }) : super(key: key);

  @override
  StatefulElement createElement() => new StatefulElement(this);

  @protected
  State createState();
}

StatefulElement間接繼承自Element類,與StatefulWidget相對應(作為其配置數(shù)據(jù))。StatefulElement中可能會多次調用createState()來創(chuàng)建狀態(tài)(State)對象。
createState()用于創(chuàng)建和Stateful widget相關的狀態(tài),它在Stateful widget的生命周期中可能會被多次調用。例如,當一個Stateful widget同時插入到widget樹的多個位置時,F(xiàn)lutter framework就會調用該方法為每一個位置生成一個獨立的State實例,其實,本質上就是一個StatefulElement對應一個State實例。

State

一個StatefulWidget類會對應一個State類,State表示與其對應的StatefulWidget要維護的狀態(tài),State中的保存的狀態(tài)信息可以:

  • widget構建時可以被同步讀取。
  • widget生命周期中可以被改變,當State被改變時,可以手動調用其setState()方法通知Flutter framework狀態(tài)發(fā)生改變,F(xiàn)lutter framework在收到消息后,會重新調用其build方法重新構建widget樹,從而達到更新UI的目的。

State中有兩個常用屬性:
1.widget,它表示與該State實例關聯(lián)的widget實例,由Flutter framework動態(tài)設置。注意,這種關聯(lián)并非永久的,因為在應用生命周期中,UI樹上的某一個節(jié)點的widget實例在重新構建時可能會變化,但State實例只會在第一次插入到樹中時被創(chuàng)建,當在重新構建時,如果widget被修改了,F(xiàn)lutter framework會動態(tài)設置State.widget為新的widget實例。

2.context。StatefulWidget對應的BuildContext,作用同StatelessWidgetBuildContext。

State生命周期

理解State的生命周期對flutter開發(fā)非常重要,為了加深讀者印象,本節(jié)我們通過一個實例來演示一下State生命周期。在接下來的示例中,我們實現(xiàn)一個計數(shù)器widget,點擊它可以使計數(shù)器加1,由于要保存計數(shù)器的數(shù)值狀態(tài),所以我們應繼承StatefulWidget,代碼如下:

class CounterWidget extends StatefulWidget {
  const CounterWidget({
    Key key,
    this.initValue: 0
  });

  final int initValue;

  @override
  _CounterWidgetState createState() => new _CounterWidgetState();
}

CounterWidget接收一個initValue整型參數(shù),它表示計數(shù)器的初始值。下面我們看一下State的代碼:

class _CounterWidgetState extends State<CounterWidget> {  
  int _counter;

  @override
  void initState() {
    super.initState();
    //初始化狀態(tài)  
    _counter=widget.initValue;
    print("initState");
  }

  @override
  Widget build(BuildContext context) {
    print("build");
    return Scaffold(
      body: Center(
        child: FlatButton(
          child: Text('$_counter'),
          //點擊后計數(shù)器自增
          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");
  }

}

接下來,我們創(chuàng)建一個新路由,在新路由中,我們只顯示一個CounterWidget

Widget build(BuildContext context) {
  return CounterWidget();
}

我們運行應用并打開該路由頁面,在新路由頁打開后,屏幕中央就會出現(xiàn)一個數(shù)字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

可以看到此時initStatedidChangeDependencies都沒有被調用,而此時didUpdateWidget被調用。

接下來,我們在widget樹中移除CounterWidget,將路由build方法改為:

Widget build(BuildContext context) {
  //移除計數(shù)器 
  //return CounterWidget();
  //隨便返回一個Text()
  return Text("xxx");
}

然后熱重載,日志如下:

I/flutter ( 5436): reassemble
I/flutter ( 5436): deactive
I/flutter ( 5436): dispose

我們可以看到,在CounterWidgetwidget樹中移除時,deactivedispose會依次被調用。
下面我們來看看各個回調函數(shù):

  • initState:當Widget第一次插入到Widget樹時會被調用,對于每一個State對象,F(xiàn)lutter framework只會調用一次該回調,所以,通常在該回調中做一些一次性的操作,如狀態(tài)初始化、訂閱子樹的事件通知等。不能在該回調中調用BuildContext.dependOnInheritedWidgetOfExactType(該方法用于在Widget樹上獲取離當前widget最近的一個父級InheritFromWidget,關于InheritedWidget我們將在后面章節(jié)介紹),原因是在初始化完成后,Widget樹中的InheritFromWidget也可能會發(fā)生變化,所以正確的做法應該在在build()方法或didChangeDependencies()中調用它。

  • didChangeDependencies():當State對象的依賴發(fā)生變化時會被調用;例如:在之前build() 中包含了一個InheritedWidget,然后在之后的build()InheritedWidget發(fā)生了變化,那么此時InheritedWidget的子widgetdidChangeDependencies()回調都會被調用。典型的場景是當系統(tǒng)語言Locale或應用主題改變時,F(xiàn)lutter framework會通知widget調用此回調。

  • build():此回調讀者現(xiàn)在應該已經相當熟悉了,它主要是用于構建Widget子樹的,會在如下場景被調用:

    1.在調用initState()之后。
    2.在調用didUpdateWidget()之后。
    3.在調用setState()之后。
    4.在調用didChangeDependencies()之后。
    5.在State對象從樹中一個位置移除后(會調用deactivate)又重新插入到樹的其它位置之后。

  • reassemble():此回調是專門為了開發(fā)調試而提供的,在熱重載(hot reload)時會被調用,此回調在Release模式下永遠不會被調用。

  • didUpdateWidget():在widget重新構建時,F(xiàn)lutter framework會調用Widget.canUpdate來檢測Widget樹中同一位置的新舊節(jié)點,然后決定是否需要更新,如果Widget.canUpdate返回true則會調用此回調。正如之前所述,Widget.canUpdate會在新舊widgetkeyruntimeType同時相等時會返回true,也就是說在在新舊widgetkeyruntimeType同時相等時didUpdateWidget()就會被調用。

  • deactivate():當State對象從樹中被移除時,會調用此回調。在一些場景下,F(xiàn)lutter framework會將State對象重新插到樹中,如包含此State對象的子樹在樹的一個位置移動到另一個位置時(可以通過GlobalKey來實現(xiàn))。如果移除后沒有重新插入到樹中則緊接著會調用dispose()方法。

  • dispose():當State對象從樹中被永久移除時調用;通常在此回調中釋放資源。
    StatefulWidget生命周期如圖3-2所示:

    StatefulWidget生命周期

注意:在繼承StatefulWidget重寫其方法時,對于包含@mustCallSuper標注的父類方法,都要在子類方法中先調用父類方法。

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內容