Flutter系列四:你真的了解StatelessWidget和StatefulWidget的區(qū)別嗎?

開(kāi)發(fā)者在進(jìn)行Flutter開(kāi)發(fā)時(shí),大部分工作基本上少不了與StatelessWidgetStatefulWidget打交道。大家是否真的了解StatelessWidgetStatefulWidget?

討論

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

列出網(wǎng)上一些文章中的解釋:

  1. 如果我們的Widget是StatelessWidget,那么當(dāng)他的內(nèi)容被創(chuàng)建出來(lái)之后,就不能再改變了。相反StatefulWidget就可以。
  2. 無(wú)狀態(tài)Widget,就是說(shuō)一旦這個(gè)Widget創(chuàng)建完成,狀態(tài)就不允許再變動(dòng)。有狀態(tài)Widget,就是說(shuō)當(dāng)前Widget創(chuàng)建完成之后,還可以對(duì)當(dāng)前Widget做更改,可以通過(guò)setState函數(shù)來(lái)刷新當(dāng)前Widget來(lái)達(dá)到有狀態(tài)。
  3. StatelessWidget是一個(gè)不需要狀態(tài)更改的widget,它沒(méi)有要管理的內(nèi)部狀態(tài)。StatefulWidget是可變狀態(tài)的widget。

如果你對(duì)上述一些觀點(diǎn)很認(rèn)同的話,我覺(jué)得閱讀本篇文章應(yīng)該可以給你提供一個(gè)不一樣的理解視角。

Widget

我們要比較StatelessWidgetStatefulWidget的區(qū)別,我們得先知道什么是Widget。

官方對(duì)Widget的解釋是:

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

Widget是部分界面的不可變的描述信息。

重要的事情說(shuō)三遍:

Widget不可變的;

Widget不可變的;

Widget不可變的。

我們從代碼上看看Widget如何實(shí)現(xiàn)的不可變。Widget的代碼如下:

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

  final Key? key;
  
  // 省略...
}

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

接下來(lái)我們?cè)倏纯?strong>StatelessWidget和StatefulWidget的官方解釋和相關(guān)代碼:

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總結(jié)
  1. StatelessWidgetStatefulWidget沒(méi)有本質(zhì)區(qū)別,他們的所有屬性都是不可變的。它們都沒(méi)法更新,除非用一個(gè)新的Widget去替換它們。
  2. StatefulWidget擁有一個(gè)可變的State。

這樣我們就得到了一個(gè)結(jié)論:StatelessWidgetStatefulWidget的區(qū)別就在這個(gè)可變的State了。

新的問(wèn)題又來(lái)了,這個(gè)State扮演了什么作用呢?

State

我們進(jìn)行界面的修改,一般會(huì)調(diào)用state.setState()方法。那這個(gè)方法是如何實(shí)現(xiàn)界面元素修改的呢?

void setState(VoidCallback fn) {
    final dynamic result = fn() as dynamic;
    _element!.markNeedsBuild();
  }

setState方法很簡(jiǎn)單:

  1. 執(zhí)行傳入的函數(shù);
  2. _element調(diào)用了markNeedsBuild方法。
void markNeedsBuild() {
    if (dirty)
      return;
    _dirty = true;
    owner!.scheduleBuildFor(this);
}
  1. _element把自己的_dirty屬性設(shè)置為true;
  2. BuildOwner調(diào)用scheduleBuildFor方法。
void scheduleBuildFor(Element element) {
    if (!_scheduledFlushDirtyElements && onBuildScheduled != null) {
      _scheduledFlushDirtyElements = true;
      // 1
      onBuildScheduled!();
    }
    _dirtyElements.add(element);
    element._inDirtyList = true;
  }
  1. BuildOwner調(diào)用onBuildScheduled方法;

內(nèi)容回顧:onBuildScheduled方法是在WidgetsBindinginitInstances中初始化的,一系列調(diào)用后最后調(diào)用的就是scheduleFrame請(qǐng)求Native Platform要刷新界面。

  1. element加入到dirtyElements中。

在合適的時(shí)候Flutter Engine會(huì)回調(diào)SchedulerBindinghandleDrawFrame方法,最后會(huì)調(diào)用BuildOwnerbuildScope方法。

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

遍歷dirtyElements元素,每個(gè)element調(diào)用rebuild。

rebuild的作用是什么?沒(méi)錯(cuò),就是我們開(kāi)頭提到的對(duì)界面元素進(jìn)行更新的操作。

刷新渲染的具體邏輯,我將會(huì)在后續(xù)文章中詳細(xì)介紹,這里沒(méi)法詳細(xì)展開(kāi)。

結(jié)論

StatelessWidgetStatefulWidget的本質(zhì)區(qū)別就是能否自我重新構(gòu)建(self rebuild)。

區(qū)別

一些思考

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

Widget依賴于構(gòu)造函數(shù)Build方法中的BuildContext中的外部信息,如果是外部觸發(fā)的Build(例如:祖先Widget build),所有信息都是完整的。如果self rebuild則無(wú)法獲取更新后的外部信息,所以需要內(nèi)部維護(hù)一份不依賴于外部的信息,State就是這個(gè)作用。

  • 既然StatefulWidget的功能更完善,為什么又提供一個(gè)StatelessWidget呢?

這個(gè)問(wèn)題其實(shí)等同于為什么官方要限制我們使用self rebuild?每次Build都需要新建和銷毀大量的Widget,Element Treediff,甚至繁重的渲染和重繪。官方推薦使用StatelessWidget,其實(shí)就是為了性能的考慮而對(duì)開(kāi)發(fā)者進(jìn)行的一些約束,限制開(kāi)發(fā)者無(wú)節(jié)制的使用self rebuild造成的性能降低。

  • 可不可以在開(kāi)發(fā)中全部都使用StatefulWidget

當(dāng)然可以,但是不推薦,理由見(jiàn)上個(gè)問(wèn)題。

  • 可不可以在開(kāi)發(fā)中全部都使用StatelessWidget

如果是顯示簡(jiǎn)單的不變的內(nèi)容可以這樣使用,但是這種場(chǎng)景太少了。至少在App應(yīng)用中不太可能。

  • 開(kāi)發(fā)中如何選擇StatelessWidget還是StatefulWidget

首選StatelessWidget,當(dāng)無(wú)法滿足需求的時(shí)候用VS Code或者Android Stutio的快捷鍵將其變成StatefulWidget

實(shí)戰(zhàn)分享

我們前面比較了StatelessWidgetStatefulWidget的區(qū)別,進(jìn)行了一些分析,到底如何寫(xiě)出更好更優(yōu)化的代碼,現(xiàn)在我們就用Flutter官方的計(jì)數(shù)器Demo來(lái)練練手。

Counter

通過(guò)前面的分析,我們知道點(diǎn)擊FloatingActionButton會(huì)調(diào)用_MyHomePageStatesetState進(jìn)行rebuild。如下圖所示:

demo_build

細(xì)心的你可能發(fā)現(xiàn)問(wèn)題了,我只是想修改Scaffold->Body->Center->Column->第二個(gè)Text中的文字。而Build的起點(diǎn)是Scaffold,這么長(zhǎng)的構(gòu)建鏈條相當(dāng)于修改一個(gè)文字,把整個(gè)頁(yè)面都重新構(gòu)建了一次。就顯然是一個(gè)無(wú)法忽視的問(wèn)題。

注意:真實(shí)的Build鏈條不是我上面列的這么短,因?yàn)?strong>Scaffold等都進(jìn)行了封裝,真實(shí)的Build得進(jìn)入他們的build方法去了解,真實(shí)的Build鏈條比我們代碼中看到的Scaffold->Body->Center->Column->第二個(gè)Text這個(gè)邏輯復(fù)雜多了。

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

我們抽提一個(gè)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),
      ), 
    );
  }

}

這樣就改造完成了。

總結(jié):

我們需要對(duì)StatelessWidgetStatefulWidget有一個(gè)全面的了解,才能正確的使用他們。歡迎一起探討和學(xué)習(xí)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請(qǐng)通過(guò)簡(jiǎn)信或評(píng)論聯(lián)系作者。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容