Widget的state 和生命周期

1,120 阅读9分钟

在 Flutter的世界里,一切都是 Widget,而 Widget有两个子类 StatelessWidget和StatefulWidget,下面一起学习这两类Widget 是什么、有什么区别、使用时如何选择。

在了解 StatelessWidget 和 StatefulWidget之前先了解 Flutter 中的UI编程范式。

UI变成范式

UI编程范式:如何调整一个控件的展示样式。有两种方式,命令式编程和声明式编程。

命令式编程:通过命令精准的控制控件的属性,如 Android 中,修改TextView 文本内容,textView.text = “newText”。

声明式编程:其核心设计思想就是将视图和数据分离,Flutter中就是使用这声明式。除了设计好 Widget 布局方案之外,还需要提前维护一套文案数据集,并为Widget绑定数据集,使 Widget 根据这个数据集完成渲染。

声明式编程的优点:当需要改变页面的文案时,只需要改变数据集中的文案,并通知Flutter框架触发widget重新渲染即可。这样一来就不用精准关注UI编程的各个过程细节,只需要维护好数据集即可。

StatefulWidget

StatelessWidget:无状态的Widget,在初始化的时候就可以确定Widget 的显示样式,之后不会再发生变化。如 Text、Container、Row、Column 等。

看看Text 的源码:

class Text extends StatelessWidget {     
  //构造方法及属性声明部分
  const Text(this.data, {
    Key key,
    this.textAlign,
    this.textDirection,
    //其他参数
    ...
  }) : assert(data != null),
     textSpan = null,
     super(key: key);
     
  final String data;
  final TextAlign textAlign;
  final TextDirection textDirection;
  //其他属性
  ...
  
  @override
  Widget build(BuildContext context) {
    ...
    Widget result = RichText(
       //初始化配置
       ...
      )
    );
    ...
    return result;
  }
}

在构造方法将其属性列表赋值后,build 方法随即将子组件 RichText 通过其属性列表(如文本 data、对齐方式 textAlign、文本展示方向 textDirection 等)初始化后返回,之后 Text 内部不再响应外部数据的变化。

使用场景:如果显示样式能在一开始就确定,之后不会响应数据的变化,就可以使用 StatelessWidget。如 每一个页面的 title 只初始化一次,后面不再发生变化,提示错误信息的dialog 等。

StatefulWidget

StatelessWidget:有状态的Widget,在初始化的时候就不可以确定Widget 的显示样式,之后可以响应数据集的变化,重新渲染UI。如,处理用户的交互(比如,用户点击按钮)或其内部数据的变化(比如,网络数据回包),并体现在 UI 上。

看看一个例子:Image的源码

class Image extends StatefulWidget {
  //构造方法及属性声明部分
  const Image({
    Key key,
    @required this.image,
    //其他参数
  }) : assert(image != null),
       super(key: key);

  final ImageProvider image;
  //其他属性
  ...
  
  @override
  _ImageState createState() => _ImageState();
  ...
}

class _ImageState extends State<Image> {
  ImageInfo _imageInfo;
  //其他属性
  ...

  void _handleImageChanged(ImageInfo imageInfo, bool synchronousCall) {
    setState(() {
      _imageInfo = imageInfo;
    });
  }
  ...
  @override
  Widget build(BuildContext context) {
    final RawImage image = RawImage(
      image: _imageInfo?.image,
      //其他初始化配置
      ...
    );
    return image;
  }
 ...
}

首先可以看到在 StatefulWidget 中没有 build 方法,而是多了一个 createState方法;通过 createState 方法把对视图的构建工作交给了 _ImageState;在 _ImageState 中,视图的信息通过 ImageInfo 来获取的。

ImageInfo 数据变化,更新Image流程:

  1. 当 ImageInfo 数据发生变化时,State 对象通过 _handleImageChanged 方法监听到 _imageInfo 属性发生了变化
  2. 调用 _ImageState 类的 setState 方法通知 Flutter 框架:“我这儿的数据变啦,请使用更新后的 _imageInfo 数据重新加载图片!”。
  3. Flutter 框架则会标记视图状态,更新 UI。

到这来可以发现 StatefulWidget 既可以响应数据的变化,又可以展示静态UI,StatelessWidget是不是有点多余了?

先看看widget 的更新机制:

Widget 是不可变的,更新则意味着销毁 + 重建(build)。
StatelessWidget 是静态的,一旦创建则无需更新;
而对于 StatefulWidget 来说,在 State 类中调用 setState 方法更新数据,会触发视图的销毁和重建,也将间接地触发其每个子 Widget 的销毁和重建。

这就意味着,如果我们的根布局是一个 StatefulWidget,在其 State 中每调用一次更新 UI,都将是一整个页面所有 Widget 的销毁和重建。

虽然 Flutter 内部通过 Element 层可以最大程度地降低对真实渲染视图的修改,提高渲染效率,而不是销毁整个 RenderObject 树重建。但是大量 Widget 对象的销毁重建是无法避免的。如果某个子 Widget 的重建涉及到一些耗时操作,那页面的渲染性能将会急剧下降。

所有 正确评估要不要使用StatefulWidget,是提高Flutter渲染性能最直接的手段。

StatelessWidget 和 StatefulWidget 如何选择?

一句话:只是其静态展示,使用StatelessWidget,否则 使用StatefulWidget。

Widget的生命周期

Flutter 中的 Widget 也存在生命周期,并且通过 State 来体现。(是对StatefulWidget来说的)

State 生命周期

State 的生命周期,指的是在用户参与的情况下,其关联的 Widget 所经历的,从创建到显示再到更新最后到停止,直至销毁等各个过程阶段。

image.png

从图中可以看到 state 的生命周期主要有三个过程:创建、更新和销毁。

创建

State 初始化时会依次执行 :构造方法 -> initState -> didChangeDependencies -> build,随后完成页面渲染

构造方法:state 生命周期的起点,由 StatefulWidget#createState方法创建,在这里可以接收到从外部传入的初始化数据。

initState,在 State 对象被插入视图树的时候调用。这个函数在 State 的生命周期中只会被调用一次,所以我们可以在这里做一些初始化工作。类似Android 中activity的onCreate方法。

didChangeDependencies 则用来专门处理 State 对象依赖关系变化,会在 initState() 调用结束后,被 Flutter 调用。

build,作用是构建视图。经过以上步骤,Framework 认为 State 已经准备好了,于是调用 build。我们需要在这个函数中,根据父 Widget 传递过来的初始化配置数据,以及 State 的当前状态,创建一个 Widget 然后返回。

更新

如何更新的?

通过三个方法:setState、didchangeDependencies 与 didUpdateWidget。

setState:一般是当数据发生变化后,通过该方法来通知flutter 框架来更新UI,开发中常用。

didchangeDependencies:State 对象的依赖关系发生变化后,Flutter 会回调这个方法,随后触发组件构建。如系统 语言的切换等。

didUpdateWidget:当 Widget 的配置发生变化时,比如,父 Widget 触发重建(即父 Widget 的状态发生变化时),热重载时,系统会调用这个函数。

销毁

组件被移除,或是页面销毁的时候,系统会调用 deactivate 和 dispose 这两个方法,来移除或销毁组件。

deactivate:当组件的可见状态发生变化时,会调用。

dispose:当 State 被永久地从视图树中移除时,这个阶段,组件就要被销毁了,所以我们可以在这里进行最终的资源释放、移除监听、清理环境,等等。类似 Android 中activity 的onDestory方法。

上面就是一state 的生命周期,和activity 的生命周期很类似。下面使用一张表格来总结,方便对比和理解。

方法名功能调用时期调用次数
构造方法接收父widget传递过来初始化UI的数据创建State1
initState预渲染相关的初始化工作State 被插入视图树时1
didchangeDependencies处理State 依赖对象发生变化时initState后或State 依赖关系发生变化时>=1
build构建视图State 准备好数据渲染时>=1
setState触发视图重建需要更新UI>=1
didchangeDependencies处理widget的配置发生变化父widget setState 触发子widget重建>=1
deactivate组件被移除组件不可见>=1
dispose组件被销毁组件被永远移除1

APP的生命周期

上面学习了State 的生命周期,而 App 的生命周期,则定义了 App 从启动到退出的全过程。在开发中常常需要在对应的APP 生命周期中处理一些事情,如 Android 中在 appLication.onCreate 方法中做一些出事操作,前台切换都后台取消网络请求等。在Flutter 中如何处理呢?

利用 WidgetsBindingObserver 类来完成生命周期的监听。


abstract class WidgetsBindingObserver {
  //页面pop
  Future<bool> didPopRoute() => Future<bool>.value(false);
  //页面push
  Future<bool> didPushRoute(String route) => Future<bool>.value(false);
  //系统窗口相关改变回调,如旋转
  void didChangeMetrics() { }
  //文本缩放系数变化
  void didChangeTextScaleFactor() { }
  //系统亮度变化
  void didChangePlatformBrightness() { }
  //本地化语言变化
  void didChangeLocales(List<Locale> locale) { }
  //App生命周期变化
  void didChangeAppLifecycleState(AppLifecycleState state) { }
  //内存警告回调
  void didHaveMemoryPressure() { }
  //Accessibility相关特性回调
  void didChangeAccessibilityFeatures() {}
}

在 WidgetsBindingObserver 中有很多接口回调,我们通过给 WidgetsBinding 的单例对象设置监听器,就可以监听对应的回调方法。具体使用可以参考官方文档

这里我们最关心的是 App 生命周期的回调 didChangeAppLifecycleState,和帧绘制回调 addPostFrameCallback 与 addPersistentFrameCallback。

didChangeAppLifecycleState 回调函数中,有一个参数类型为 AppLifecycleState 的枚举类,这个枚举类是 Flutter 对 App 生命周期状态的封装。它的常用状态包括 resumed、inactive、paused 这三个。

  • resumed:可见的,并能响应用户的输入。
  • inactive:处在不活动状态,无法处理用户响应。
  • paused:不可见并不能响应用户的输入,但是在后台继续活动中。

来一个简单的例子:在新建的Flutter_Demo中,在 initState注册监听,在dispose 移除监听,然后分别从后台切换到前台、前台切换到后台,看看有什么变化。

class _MyHomePageState extends State<MyHomePage> with WidgetsBindingObserver {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @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:',
            ),
            Text(
              '$_counter',
              style: Theme
                  .of(context)
                  .textTheme
                  .headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }

  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    WidgetsBinding.instance.addObserver(this);
  }

  @override
  void dispose() {
    // TODO: implement dispose
    super.dispose();
    WidgetsBinding.instance.removeObserver(this);
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    // TODO: implement didChangeAppLifecycleState
    super.didChangeAppLifecycleState(state);
    print("didChangeAppLifecycleState::state=$state");
  }
}
  • 从前台到后台,按back键:AppLifecycleState.inactive->AppLifecycleState.paused->AppLifecycleState.detached
  • 从前台到后台,按home键:AppLifecycleState.inactive->AppLifecycleState.paused
  • 从后台到前台:AppLifecycleState.resumed

帧绘制回调

WidgetsBinding 提供了单次 Frame 绘制回调,以及实时 Frame 绘制回调两种机制,来分别满足不同的需求:

  • 单次 Frame 绘制回调,通过 addPostFrameCallback 实现。它会在当前 Frame 绘制完成后进行进行回调,并且只会回调一次,如果要再次监听则需要再设置一次。

在开发中有这样的需求,当页面可见后,再做其他操作,这时候就可以通过监听,得到第一帧回调后,再做其他事情。

WidgetsBinding.instance.addPostFrameCallback((_){ print("单次Frame绘制回调");//只回调一次 });
  • 实时 Frame 绘制回调,则通过 addPersistentFrameCallback 实现。这个函数会在每次绘制 Frame 结束后进行回调,可以用做 FPS 监测。
WidgetsBinding.instance.addPersistentFrameCallback((_){
  print("实时Frame绘制回调");//每帧都回调
});

使用的场景,一个小例子,在画泡泡上升的动画,我可以用Timer来画,但是我手机性能好的话,一帧结束了。还有等待一段时间才绘制下一帧;手机性能差的话,我前一帧都没画完呢,你就开始下一帧了,这不是要卡死的节奏吗?
这时候就可以通过 addPersistentFrameCallback 来监听,完成这一帧后,再绘制下一帧,这比定时器好多了。

一些学习网站

参考链接