Flutter系列四:你真的瞭解StatelessWidget和StatefulWidget的區別嗎?

chonglingliu發表於2021-03-21

開發者在進行Flutter開發時,大部分工作基本上少不了與StatelessWidgetStatefulWidget打交道。大家是否真的瞭解StatelessWidgetStatefulWidget?

討論

我閱讀了很多網上的文章,大部分會講解兩者的使用上的區別,一部分文章有解釋這兩者的區別。但是他們的解釋有的是字面解釋,有的是淺嘗輒止,有的甚至是有一定的誤導。

列出網上一些文章中的解釋:

  1. 如果我們的Widget是StatelessWidget,那麼當他的內容被建立出來之後,就不能再改變了。相反StatefulWidget就可以。
  2. 無狀態Widget,就是說一旦這個Widget建立完成,狀態就不允許再變動。有狀態Widget,就是說當前Widget建立完成之後,還可以對當前Widget做更改,可以通過setState函式來重新整理當前Widget來達到有狀態。
  3. StatelessWidget是一個不需要狀態更改的widget,它沒有要管理的內部狀態。StatefulWidget是可變狀態的widget。

如果你對上述一些觀點很認同的話,我覺得閱讀本篇文章應該可以給你提供一個不一樣的理解視角。

Widget

我們要比較StatelessWidgetStatefulWidget的區別,我們得先知道什麼是Widget

官方對Widget的解釋是:

A widget is an immutable description of a part of a user interface.

Widget是部分介面的不可變的描述資訊

重要的事情說三遍:

Widget不可變的;

Widget不可變的;

Widget不可變的。

我們從程式碼上看看Widget如何實現的不可變。Widget的程式碼如下:

@immutable
abstract class Widget extends DiagnosticableTree {
  
  const Widget({ this.key });

  final Key? key;
  
  // 省略...
}
複製程式碼

我們可以看到Widget左上角有一個@immutable註解,這個註解的意思是所有的屬性必須是final修飾,也就是Widget一旦初始化以後,其屬性將不可變。

接下來我們再看看StatelessWidgetStatefulWidget的官方解釋和相關程式碼:

StatelessWidget --- A widget that does not require mutable state.

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

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

  @protected
  Widget build(BuildContext context);
}
複製程式碼

StatefulWidget --- A widget that has mutable state.

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

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

  @protected
  @factory
  State createState();
}
複製程式碼
Widget總結
  1. StatelessWidgetStatefulWidget沒有本質區別,他們的所有屬性都是不可變的。它們都沒法更新,除非用一個新的Widget去替換它們。
  2. StatefulWidget擁有一個可變的State

這樣我們就得到了一個結論:StatelessWidgetStatefulWidget的區別就在這個可變的State了。

新的問題又來了,這個State扮演了什麼作用呢?

State

我們進行介面的修改,一般會呼叫state.setState()方法。那這個方法是如何實現介面元素修改的呢?

void setState(VoidCallback fn) {
    final dynamic result = fn() as dynamic;
    _element!.markNeedsBuild();
  }
複製程式碼

setState方法很簡單:

  1. 執行傳入的函式;
  2. _element呼叫了markNeedsBuild方法。
void markNeedsBuild() {
    if (dirty)
      return;
    _dirty = true;
    owner!.scheduleBuildFor(this);
}
複製程式碼
  1. _element把自己的_dirty屬性設定為true;
  2. BuildOwner呼叫scheduleBuildFor方法。
void scheduleBuildFor(Element element) {
    if (!_scheduledFlushDirtyElements && onBuildScheduled != null) {
      _scheduledFlushDirtyElements = true;
      // 1
      onBuildScheduled!();
    }
    _dirtyElements.add(element);
    element._inDirtyList = true;
  }
複製程式碼
  1. BuildOwner呼叫onBuildScheduled方法;

內容回顧:onBuildScheduled方法是在WidgetsBindinginitInstances中初始化的,一系列呼叫後最後呼叫的就是scheduleFrame請求Native Platform要重新整理介面。

  1. element加入到dirtyElements中。

在合適的時候Flutter Engine會回撥SchedulerBindinghandleDrawFrame方法,最後會呼叫BuildOwnerbuildScope方法。

void buildScope(Element context, [ VoidCallback? callback ]) {
    int dirtyCount = _dirtyElements.length;
    int index = 0;
    while (index < dirtyCount) {
        _dirtyElements[index].rebuild();
    }
}
複製程式碼

遍歷dirtyElements元素,每個element呼叫rebuild

rebuild的作用是什麼?沒錯,就是我們開頭提到的對介面元素進行更新的操作。

重新整理渲染的具體邏輯,我將會在後續文章中詳細介紹,這裡沒法詳細展開。

結論

StatelessWidgetStatefulWidget的本質區別就是能否自我重新構建(self rebuild)。

區別

一些思考

  • 既然StatefulWidget的主要作用只是為了賦予了其自我重新構建(self rebuild)的能力,那為什麼需要State呢?

Widget依賴於建構函式Build方法中的BuildContext中的外部資訊,如果是外部觸發的Build(例如:祖先Widget build),所有資訊都是完整的。如果self rebuild則無法獲取更新後的外部資訊,所以需要內部維護一份不依賴於外部的資訊,State就是這個作用。

  • 既然StatefulWidget的功能更完善,為什麼又提供一個StatelessWidget呢?

這個問題其實等同於為什麼官方要限制我們使用self rebuild?每次Build都需要新建和銷燬大量的WidgetElement Treediff,甚至繁重的渲染和重繪。官方推薦使用StatelessWidget,其實就是為了效能的考慮而對開發者進行的一些約束,限制開發者無節制的使用self rebuild造成的效能降低。

  • 可不可以在開發中全部都使用StatefulWidget

當然可以,但是不推薦,理由見上個問題。

  • 可不可以在開發中全部都使用StatelessWidget

如果是顯示簡單的不變的內容可以這樣使用,但是這種場景太少了。至少在App應用中不太可能。

  • 開發中如何選擇StatelessWidget還是StatefulWidget

首選StatelessWidget,當無法滿足需求的時候用VS Code或者Android Stutio的快捷鍵將其變成StatefulWidget

實戰分享

我們前面比較了StatelessWidgetStatefulWidget的區別,進行了一些分析,到底如何寫出更好更優化的程式碼,現在我們就用Flutter官方的計數器Demo來練練手。

Counter

通過前面的分析,我們知道點選FloatingActionButton會呼叫**_MyHomePageStatesetState進行rebuild**。如下圖所示:

demo_build

細心的你可能發現問題了,我只是想修改Scaffold->Body->Center->Column->第二個Text中的文字。而Build的起點是Scaffold,這麼長的構建鏈條相當於修改一個文字,把整個頁面都重新構建了一次。就顯然是一個無法忽視的問題。

注意:真實的Build鏈條不是我上面列的這麼短,因為Scaffold等都進行了封裝,真實的Build得進入他們的build方法去了解,真實的Build鏈條比我們程式碼中看到的Scaffold->Body->Center->Column->第二個Text這個邏輯複雜多了。

修改的思路就是我們只需要在第二個Text上封裝一個StatefulWidget,讓這個StatefulWidgetsetState去觸發第二個Text的文字修改。

我們抽提一個CounterText

class CounterText extends StatefulWidget {

  final _CounterTextState state = _CounterTextState();

  CounterText({
    Key key,
  }) : super(key: key);

  @override
  _CounterTextState createState() => state;
}

class _CounterTextState extends State<CounterText> {

  int _counter = 0;
  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  } 

  @override
  Widget build(BuildContext context) {
    return Container(
      child: Text(
        '$_counter',
        style: Theme.of(context).textTheme.headline4,
      ),
    );
  }
}
複製程式碼

CounterText的使用:

  MyHomePage({Key key, this.title}) : super(key: key);
  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  CounterText counterText = CounterText();
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            counterText,
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: counterText.state._incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ), 
    );
  }

}
複製程式碼

這樣就改造完成了。

總結:

我們需要對StatelessWidgetStatefulWidget有一個全面的瞭解,才能正確的使用他們。歡迎一起探討和學習。

相關文章