Flutter StatefulWidget和StatelessWidget的區別和使用以及更深入的思考

白瑞德發表於2019-09-12

拋磚引玉

自從開始使用Flutter,接觸最多的東西肯定少不了StatefulWidgetStatelessWidget。我本人在學習和了解它們的過程中也翻閱了大量的文件和資料,但發現他們都在講二者的區別和使用場景以及案例——但是為什麼要這麼用呢?這是一個值得思考的問題。

StatefulWidget和StatelessWidget簡介

免不了俗,開篇也是先講一下StatefulWidgetStatelessWidget的用法和區別吧。 Flutter中,一切皆Widget。Widget是檢視的載體,而Widget包含兩種,一種是不需要更改狀態的Widget,StatelessWidget它沒要需要管理的內部狀態,是無狀態的。另外一種是可變狀態的,StatefulWidget它有需要管理的內部狀態,使用setState來管理狀態改變。 Widget是有狀態的還是無狀態的,取決於他們依賴於狀態的變化:

  • 有狀態:互動或者資料改變導致Widget改變,例如改變文案
  • 無狀態:不會被改變的Widget,例如純展示頁面,資料也不會改變

特別提示

我特意用一個標題來吸引大家注意,是因為我在好幾篇部落格看到了類似下面的話:

Flutter 裡面包含兩種widget,一種是不可變的Widget——StatelessWidget,另外一種是可變的Widget——StatefulWidget

這是大錯特錯的!!!,因為Widget只是檢視的“配置資訊”,是資料的對映, Widget是不可變的,不可變的!!。變的只是Widge裡面的狀態,也就是State。 貼一段Widget原始碼的截圖

Flutter StatefulWidget和StatelessWidget的區別和使用以及更深入的思考
注意其中的“A widget is an immutable description of part of a user interface”。Widget只是使用者介面一部分不可變的描述——至於為什麼不可變以及都不可變了還怎麼重新整理UI,這兩個問題接下來我會用一片部落格詳細介紹一下Flutter的渲染機制。

StatelessWidget

StatelessWidget是一個沒有狀態的widget——沒有要管理的內部狀態。它通過構建一系列其他小部件來更加具體地描述使用者介面,從而描述使用者介面的一部分。當我們的頁面不依賴Widget物件本身中的配置資訊以及BuildContext時,就可以用到無狀態元件。例如當我們只需要顯示一段文字時。實際上Icon、Divider、Dialog、Text等都是StatelessWidget的子類。 StatelessWidget的基本使用如下:

class Less extends StatelessWidget {
  final String text;

  const Less({Key key, this.text}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return new Text(text);
  }
}
複製程式碼

Less包含了一個從外部接受一個不可變的資料來源text並將它顯示。 無狀態的元件的宣告週期只有一個:build,它只會在三種情況下被呼叫:

  • 將widget插入樹中的時候,也就是第一次構建
  • 當widget的父級更改了其配置時,例如,Less的父類改變了text的值
  • 當它依賴的InheritedWidget發生變化時

StatefulWidget

StatefulWidget是可變狀態的widget。使用setState方法管理StatefulWidget的狀態的改變。呼叫setState通知Flutter框架某個狀態發生了變化,Flutter會重新執行build方法,應用程式變可以顯示最新的狀態。 狀態是在構建widget的時候,widget可以同步讀取的資訊,而這些狀態會發生變化。要確保在狀態改變的時候即使通知widget進行動態更改,就需要用到StatefulWidget。例如一個計數器,我們點選按鈕就要讓數字加一。在Flutter中,Checkbox、FadeImage等都是有狀態元件。 StatefulWidget的基本使用如下:

class Full extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    // TODO: implement createState
    return _Full();
  }
}

class _Full extends State<Full> {
  int count = 0;

  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return new GestureDetector(
      onTap: onClick,
      child: new Text("$count"),
    );
  }

  void onClick() {
    setState(() {
      count += 1;
    });
  }
}
複製程式碼

Full包含了一個內部持有的int狀態,每次點選自增一,平使用setState重新整理頁面顯示最新的值。 StatefulWidget的生命週期比較複雜,有興趣的可以去看我的另一篇部落格:Flutter檢視Widget生命週期

StatefulWidget和StatelessWidget的實用場景

在涉及到Widget的工作時,遇到的頭等大事就是確定widget應該使用StatefulWidget還是StatelessWidget? 簡單的說,如果不需要自己維持狀態就使用StatelessWidget,否則使用StatefulWidget。 進一步分析,根據上文的介紹,我們不難發現:

  • 如果使用者互動或資料改變導致widget改變,那麼它就是有狀態的。
  • 如果一個widget是最終的或不可變的,那麼它就是無狀態。

而狀態的管理可能有三種方式——自己管理,父widget管理以及兩者混合搭配。我們可以參考下面的規則選擇Widget:

  • 如果widget的狀態取決於動作,那麼最好是由widget自身來管理狀態,也就是使用StatefulWidget,例如動畫;
  • 如果狀態是使用者資料,則最好用父widget管理,也即是使用StatelessWidget,例如一個列表單個Item的選中狀態;
  • 如果還是搖擺不定,別問,問就是StatelessWidget

更進一步的思考

前面我們已經知道了,Widget是不可變的,如果要改變就要重新建立。而StatefulWidget使用State來通過控制自身狀態來為自己標記狀態,這樣就可以在下一次系統重繪檢查時重新建立。

為什麼要選用StatelessWidget

通過上面的介紹,大家不難發現StatefulWidget幾乎是這樣一個存在——我在任何需求下使用它都能實現想要的效果,那麼我們為什麼不一股腦全部使用它呢?既然它也能實現StatelessWidget的效果,那我們還要StatelessWidget做什麼?StatefulWidget就是一個全能的存在啊!! 為了解釋這個疑問,我們就要去了解一下StatefulWidget伴隨著全能而來的代價! 首先我們粗略的追溯一下setState的重新整理原始碼:

@protected
void setState(VoidCallback fn) {
    ...
    _element.markNeedsBuild();
}

void markNeedsBuild() {
    ...
    if (dirty)
      return;
    _dirty = true;
    owner.scheduleBuildFor(this);
}

void scheduleBuildFor(Element element) {
    ...
    if (!_scheduledFlushDirtyElements && onBuildScheduled != null) {
      _scheduledFlushDirtyElements = true;
      onBuildScheduled();
    }
    _dirtyElements.add(element);
    element._inDirtyList = true;
    ...
}
複製程式碼

去掉所有的assert校驗,只保留關鍵程式碼,我們發現setState會呼叫elementmarkNeedsBuild方法,用來標記當前element為dirty狀態,也就是需要build。並執行BuildOwnerscheduleBuildFor方法,BuildOwner是負責管理element的。直接追溯到onBuildScheduled,該發放的實現為widget/binding.dart中的_handleBuildScheduled方法,其中呼叫了scheduler/binding.dart中的ensureVisualUpdate,最後呼叫了scheduleFrame方法:

void scheduleFrame() {
    if (_hasScheduledFrame || !_framesEnabled)
      return;
    assert(() {
      if (debugPrintScheduleFrameStacks)
        debugPrintStack(label: 'scheduleFrame() called. Current phase is $schedulerPhase.');
      return true;
    }());
    window.scheduleFrame();
    _hasScheduledFrame = true;
  }
複製程式碼

關鍵的程式碼出現了:window.scheduleFrame() 這是一個Native方法:

Flutter StatefulWidget和StatelessWidget的區別和使用以及更深入的思考

實際上setState只是用來標記state物件需要根據已經變更的狀態重新build來建立新的widget。呼叫setState將會出發每個子Widget的構造方法以及build方法。這意味著如果根佈局是一個StatefulWidget,那麼setState之後,整個頁面所有的widget都會重建。 通過程式碼來驗證一下:

class FulBackPage extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    // TODO: implement createState
    return _FulBackPage();
  }
}

class _FulBackPage extends State<FulBackPage> {
  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return new Column(
      children: <Widget>[
        Full(name: "A"),
        Full(name: "B"),
        Full(name: "C"),
        Full(name: "D"),
        Less(name: "E"),
        GestureDetector(
          onTap: () {
            setState(() {});
          },
          child: Text("點選"),
        )
      ],
    );
  }
}

class Full extends StatefulWidget {
  final String name;

  Full({Key key, this.name}) : super(key: key) {
    print("有狀態元件$name:建立了");
  }

  @override
  State<StatefulWidget> createState() {
    // TODO: implement createState
    return _Full();
  }
}

class _Full extends State<Full> {
  @override
  Widget build(BuildContext context) {
    print("有狀態元件${widget.name}:build了");
    return new GestureDetector(
      onTap: () {
        setState(() {});
      },
      child: new Text(widget.name),
    );
  }
}

class Less extends StatelessWidget {
  final String name;

  Less({Key key, this.name}) : super(key: key){
    print("無狀態元件$name:建立了");
  }

  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    print("無狀態元件$name:build了");
    return new Text(name);
  }
}
複製程式碼

每次點選FulBackPage的按鈕重新整理頁面,日誌輸出如下:

Flutter StatefulWidget和StatelessWidget的區別和使用以及更深入的思考
哈哈哈,發現了一個StatelessWidget的用處了。因為StatelessWidget沒有setState方法,所有它可以強制的減少開發者濫用setState,導致過多的頁面被重新整理。官方也是推薦首選使用StatelessWidget,也就是說要減少頁面重新整理的區域和層級!!! 好失望了,難道設計出StatelessWidget就是為了規範開發者的行為嗎????

Flutter StatefulWidget和StatelessWidget的區別和使用以及更深入的思考

我編不下去了啊!!!翻了翻兩個widget的build原始碼,除了一個多了個state之外,我也沒發現什麼端倪。 總之:

  • 優先使用StatelessWidget
  • 含有大量子Widget(如根佈局、次根佈局)最好使用StatelessWidget
  • StatefulWidget最好用在子節點,同時儘量減少它的子節點。

總結

開篇丟擲的問題我還是沒有徹底想明白:

Flutter StatefulWidget和StatelessWidget的區別和使用以及更深入的思考

相關文章