StatelessWidget和StatefulWidget的区别

Posted 一叶飘舟

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了StatelessWidget和StatefulWidget的区别相关的知识,希望对你有一定的参考价值。

开发者在进行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来练练手。

通过前面的分析,我们知道点击FloatingActionButton会调用**_MyHomePageStatesetState进行rebuild**。如下图所示:

细心的你可能发现问题了,我只是想修改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有一个全面的了解,才能正确的使用他们。欢迎一起探讨和学习。


延伸

State的生命周期

上面我们研究了StatelessWidgetStatefulWidget的区别,即StatefulWidget持有State能进行self Build。本篇文章我们来研究下State的生命周期。

createState

Flutter的启动流程分析这篇文章我们分析过,Widget对应的Element插入Element Tree这一个过程是在inflateWidget方法中实现的:

Element inflateWidget(Widget newWidget, dynamic newSlot) 
    final Key? key = newWidget.key;
    final Element newChild = newWidget.createElement();
    newChild.mount(this, newSlot);
    return newChild;
  

createElement()是创建的一个StatefulElement对象:

<!-- StatefulWidget -->
StatefulElement createElement() => StatefulElement(this);

接下来我们看StatefulElement(this)的逻辑:

<!-- StatefulElement -->
StatefulElement(StatefulWidget widget)
      : state = widget.createState(),
        super(widget) 
    state._element = this;
    state._widget = widget;


final State<StatefulWidget> state;
  1. 调用了widget.createState()方法生成了一个State对象,StatefulElement对象持有了这个state
  2. state又持有了StatefulElement对象,也就是说它们存在循环引用关系。
  3. state也持有了StatefulWidget对象。

三者的引用逻辑有点绕,用一个图表示如下:

我们看到StatefulElement不需要直接引用StatefulWidget,因为它持有了State,从而间接持有了StatefulWidget

StatelessElement需要直接持有StatelessWidget

mounted

<!-- State -->
bool get mounted => _element != null;

_element被赋值后,mounted就变为true了。

initState

接上面的inflateWidget方法,新创建的element对象会调用mount方法。

由于StatefulElement没有重写mount方法,实际调用的是父类ComponentElementmount方法。

<!-- ComponentElement -->
void mount(Element? parent, dynamic newSlot) 
    super.mount(parent, newSlot);
    _firstBuild();

复制代码

StatefulElement_firstBuild方法中调用了state.initState()

<!-- StatefulElement -->
void _firstBuild() 
    try 
      // 1
      final dynamic debugCheckForReturnedFuture = state.initState() as dynamic;
     finally 
    
    state.didChangeDependencies();
    super._firstBuild();

didChangeDependencies

从上面的代码,我们可以看到在_firstBuild中调用了didChangeDependencies方法,也就是说initStatedidChangeDependencies这两个方法在_firstBuild中是一起执行的。

我们看到performRebuild的时候,StatefulElement_didChangeDependencies变为true后,state也会调用didChangeDependencies方法。

<!-- StatefulElement -->
void performRebuild() 
    if (_didChangeDependencies) 
      state.didChangeDependencies();
      _didChangeDependencies = false;
    
    super.performRebuild();

InheritedWidget的值变化后,有可能会将_didChangeDependencies的值变为true。

build

我们看看super.performRebuild()方法:

<!-- ComponentElement -->
void performRebuild() 
    Widget? built;
    try 
      built = build();
    finally 

    
    _child = updateChild(_child, built, slot);


<!-- StatefulElement -->
Widget build() => state.build(this);

StatefulElementperformRebuild方法中调用了build方法,也就是state调用了build方法。

didUpdateWidget

void update(StatefulWidget newWidget) 
    super.update(newWidget);
    final StatefulWidget oldWidget = state._widget!;
    _dirty = true;
    state._widget = widget as StatefulWidget;
    try 
      final dynamic debugCheckForReturnedFuture = state.didUpdateWidget(oldWidget) as dynamic;
     finally 
    
    rebuild();
  

element调用update时候,state会调用didUpdateWidget方法。 在父Widget引起重构Build的时候,如果element能够复用就会调用update方法。

setState

这个方法是开发者调用的,只要是mounted为true,就能够进行Self rebuild

deactivate

elementactive变为inactive的时候会调用statedeactivate方法。

void deactivate() 
    state.deactivate();
    super.deactivate();

此时,elementElement tree中移除了,但是若果能被复用有可能会重新加到Element tree中。

dispose

elementinactive变为defunct的时候,会调用statedispose方法。

void unmount() 
    super.unmount();
    state.dispose();
    state._element = null;

mounted == false

由于执行了state._element = null;,所以这时候mounted就变为了falsestate将会被垃圾回收器回收。

总结

  1. StatefulWidget实例化StatefulElement对象的时候会调用widget.createState()创建State对象,然后StateStatefulElement相互引用;
  2. State具有对StatefulElement的引用后,mounted就变为true, 此时就可以调用setState方法了;
  3. StatefulElement对象然后调用mount方法挂载到Element Tree上去。挂载后时会调用initStatedidChangeDependencies方法;
  4. 然后element会调用performRebuild方法,此时会调用Statebuild方法,如果InheritedWidget的值变化也会调用didChangeDependencies方法;
  5. 父Widget引起重构Build的时候,如果element能够复用就会调用StatedidUpdateWidget方法;
  6. elementactive变为inactive的时候会调用statedeactivate方法;
  7. elementinactive变为defunct的时候,会调用statedispose方法,并将_element置为null, 此时就不能再调用setState方法了;
  8. 等待垃圾回收器回收。


 

以上是关于StatelessWidget和StatefulWidget的区别的主要内容,如果未能解决你的问题,请参考以下文章

Flutter控件——常用控件:StatelessWidget和Context

StatefulWidget 和 StatelessWidget 的 Flutter 性能

我在尝试转换我的 StatelessWidget 代码时遇到问题。为啥?

StatelessWidget 与在性能方面返回 Widget 的函数

具有覆盖构建功能的类扩展 StatelessWidget 和具有我自己的功能的常规类有啥区别

如何将数据从 StatelessWidget 传递到 StatefulWidget?