Flutter 为什么 StatefulWidget 被分成 StatefulWidget 和 State 两部分

43 min read

StatefulWidget 及 State

看下 MyApp 的实现代码:

class MyApp extends StatefulWidget {
  // This widget is the root of your application.

  @override
  State<StatefulWidget> createState() {
    // TODO: implement createState
    return MyAppState("Hello World");
  }
}

class MyAppState extends State<MyApp> {

  @override
  Widget build(BuildContext context) {
    return ...
  }
 
}

可以看到实现 StatefulWidget,需要两部分组成:

  1. StatefulWidget
  2. State

1. StatefulWidget

class MyApp extends StatefulWidget {
  // This widget is the root of your application.

  String content;

  MyApp(this.content);

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

StatefulWidget 实现步骤:

  1. 首先继承 StatefulWidget
  2. 实现 createState() 的方法,返回一个 State

StatefulWidget 的主要功能就是创建 State。

2. State

State 即是状态。

class MyApState extends State<MyApp> {

  void increment(){
    setState(() {
      widget.content += "d";
    });
  }
  
  @override
  Widget build(BuildContext context) {
    return ...
  }
}

State 的实现步骤:

  1. 首先继承 State,State 的泛型类型是上面定义的 Widget 的类型
  2. 实现 build() 的方法,返回一个 Widget
  3. 需要更改数据,刷新 UI 的话,调用 setState()

State 的定义

State 用到了泛型,它的定义是这样子的:

State<T extends StatefulWidget>

State 的功能

State 有两个功能:

  1. build() —— 创建 Widget
  2. setState() —— 刷新 UI

1. build() —— 创建Widget

State 的 build() 函数创建 Widget,用于显示 UI。

2. setState() —— 更新状态,刷新 UI

调用 setState() 方法,在 setState() 里更改数据的值,然后 setState() 会触发 State 的 build() 方法,引起强制重建 Widget,重建 Widget 的时候会重新绑定数据,
而这时数据已经发生变化,从而达到刷新 UI 的目的。

setState() 在 State 里很重要,接下来在单独讲一下 setState() 的使用。

首先看一下,setState() 在源码里的定义如下:

 @protected
  void setState(VoidCallback fn) {
    ...
  }

setState() 里要传入一个无参的函数,所以使用方法如下:

setState(() {
    widget.content += "d";
});

在无参函数内部,对要刷新的数据进行更改。

我们可以看一下 setState() 的源码,去掉没有必要的代码,就是:

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

第一行代码,执行无参函数 fn(),并把结果类型转换为 dynamic,并赋值给 result。

第二行代码会触发 Widget 创建。

这里要注意,更改数据的代码必选在 setState() 之前写,或者在  setState() 内的无参函数里写,才能刷新数据,否则是没有用的。

这里还有一个问题,Text 是 MyApp 的 子Widget,但 Text 是 StatelessWidget,为什么 Text 的内容可以改变?

setState() 可以刷新UI的原理是,setState() 会触发 StatefulWidget 强制重建,重建的时候会重新创建 Widget 和绑定数据,从而实现了刷新 UI。所以只要 MyApp 是 StatefulWidget,那么它的子类在 setState() 的作用下都可以被强制刷新。

State 的成员变量

State 里面有三个重要的成员变量:

  1. widget
  2. context
  3. mounted

1. widget

widget 是 State 的成员变量,它的类型是 Widget,前面的代码里你可能注意到了,有这种使用用法:

child: Text(widget.content)

widget 可以访问 StatefulWidget 中的成员变量。

2. context

context 也是 State 的成员变量,它的类型是 BuildContext,它的一种用法如下:

Widget build(BuildContext context) 

BuildContext 是 Flutter 里的重要概念。

3. mounted

mounted 是 bool 类型,表示当前 State 是否加载到树里。State 对象创建之后,initState() 创建之前,framework 通过与 BuildContext 相关联,来将 State 对象加载到树中,此时 mounted 会变为 true,当 State dispose 之后,mounted 就变为 false。

mounted 属性很有用,因为 setState() 只有在 mounted 为 true 的时候才能用,当 moundted 为 false 时调用会抛异常。

因为 State 的状态比较复杂,如果 setState() 使用不注意,很容易抛异常,所以保险起见,mounted 一般这么用:

if(mounted){
    setState((){
        ...
    })
}

只有在确定 State mounted 之后,才调用 setState()。

为什么 StatefulWidget 被分成 StatefulWidget 和 State 两部分?

一方面是为了保存当前 APP 的状态,另一个重要的原因就是为了性能

当 UI 需要更新时候,假设 Widget 和 State 都重建,可是 State 里保存了 UI 显示的数据,State 重建,创建新的实例,UI 之前的状态就会丢失,导致 UI 显示异常,所以要分成两部分,一部分会重建,一部分不会重建,重建的部分就是 StatefulWidget,不会重建的部分就是 State。

Widget 重建的成本很低,但 State 的重建成本很高,因此将 StatefulWidget 分成两部分:重建成本低的 Widget 和重建成本高的 State。这样就使得 State 不会被频繁重建,也就提高了性能。

StatefulWidget 的重新定义

StatefulWidget 是有 State(状态) 的Widget,当 Widget 在运行时需要改变时,就要用 StatlefulWidget。

StatefulWidget 的生命周期

因为 StatefulWidget 由 StatefulWidget 和 State 两部分组成,所以也有 StatefulWidget 的生命周期和 State 生命周期。

StatefulWidget 的生命周期很简单,只有一个,即 createState 函数:

  • createState (createState函数)

State 的生命周期

  • moundted is true

    mounted 是 boolean,只有当mounted 为 true 时,才能使用 setState()。

  • initState

    initState() 方法是在创建 State 对象后要调用的第一个方法(在构造函数之后)。

    一旦 initState() 方法完成,State 对象就初始化完成了,BuildContext 也可以用了。所以如果你要用 BuildContext,那么需要在 initState() 之后的生命周期里用到。

    可以在这里执行其的他初始化,例如执行依赖于 BuildContext 或 Widget 的初始化,或者 animations、 controllers 等动画相关的初始化。

    如果你要重写此方法,需要首先调用 super.initState() 方法。

  • didChangeDependencies

    initState() 方法运行完后,就立即运行 didChangeDependencies() 方法。

    当 Widget 依赖的数据被调用时,此方法也会被调用。

    此外,请注意,如果您的 Widget 链接到 InheritedWidget,则每次重建此窗口小部件时都会调用此方法。

    如果重写此方法,则应首先调用 super.didChangeDependencies()。

  • build

    build() 方法在 didChangeDependencies()(或者 didUpdateWidget() )之后调用。 这是构建Widget的地方。

    每次 State 对象更改时(或者当 InheritedWidget 需要通知“已注册”的小部件时)都会调用此方法!

    为了强制重建,需要调用 setState() 方法。

    至此,一个 Widget 从创建到显示的声明周期就完成了,如果在对应的方法里加上 log,会看到如下的 log输出:

    Launching lib/main.dart on iPhone XR in debug mode...
    Xcode build done.                                            3.9s
    flutter: initState
    flutter: didChangeDependencies
    flutter: build
    
  • setState()

    当状态有变化,需要刷新UI的时候,就调用 setState(),会触发强制重建 Widget。

  • didUpdateWidget()

    当 Widget 重建后,新的 Widget 会和旧的 Widget 进行对比,如果新的 Widget 和旧的 Widget 的 runtimeTypeWidget.key 都一样,那么就会调用 didUpdateWidget()。

    在 didUpdateWidget() 里,会把新的 Widget 的配置赋值给 State,相当于重新 initState() 了一次。

    调用完这个方法之后,再去调用 build() 方法。

    至此 setState() 的生命周期也完成了,会看到如下的 log 输出:

    didUpdateWidget
    build
    
  • deactive

    当 State 从树中移除时,就会触发 deactive。但是如果在这帧结束前,如果有其他地方使用到了这个Widget,就会重新把 Widget 插入到树里,这就涉及到了 Widget 的重用,Widget 的重用和 Key 有关。

    这里使用不同的方法重用,会有不同的生命周期,所以这里使用的是虚线表示的。

  • dispose

    当 StaefulWidget 从树中移除时调用 dispose() 方法。

    可以在这里执行一些清理逻辑(例如侦听器),重写此方法时,需要首先调用 super.dispose()。

    至此完成了 Widget 销毁的生命周期,log 输出如下:

    deactive
    dispose
    
  • mounted is false

    State 对象不能 remounted,所以一旦 mounted is false,就不能在使用 setState() ,会抛异常。

  • State HotReload 的生命周期 -- reassemble

    在开发期间,执行 HotReload,就会触发 reassemble(),这提供了重新初始化在 initState() 方法中准备的任何数据的机会,包括全局变量。

    前面讲了,全局变量不能用 HotReload,但是可以在 reassemble() 里改值,但是并没有卵用,因为这个只会在 Debug 阶段 Hot Reload 的时候触发。

    更改 reassemble() 里的 content 的值,然后执行 Hot Reload,输出:

    reassemble
    didUpdateWidget
    build
    

State 的生命周期在代码中对应的方法如下:

class XXXState extends State<XXX> {

  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    print("initState");
    context.runtimeType;
  }

  @override
  void didChangeDependencies() {
    // TODO: implement didChangeDependencies
    super.didChangeDependencies();
    print("didChangeDependencies");
  }
  
  @override
  void didUpdateWidget(MyApp oldWidget) {
    // TODO: implement didUpdateWidget
    super.didUpdateWidget(oldWidget);
    print("didUpdateWidget");
  }


  @override
  Widget build(BuildContext context) {
    print("build");
    return ...
  }

  @override
  void dispose() {
    // TODO: implement dispose
    super.dispose();
    print("dispose");
  }

  @override
  void reassemble() {
    // TODO: implement reassemble
    super.reassemble();
    print("reassemble");
  }

}