Flutter Widget生命周期详解

1 Widget 简介

在Flutter中,一切皆是Widget(组件),Widget的功能是“描述一个UI元素的配置数据”,它就是说,Widget其实并不是表示最终绘制在设备屏幕上的显示元素,而它只是描述显示元素的一个配置数据。

实际上,Flutter中真正代表屏幕上显示元素的类是 Element,也就是说Widget 只是描述 Element 的配置数据。并且一个 Widget 可以对应多个 Element,因为同一个 Widget 对象可以被添加到 UI树的不同部分,而真正渲染时,UI树的每一个 Element 节点都会对应一个 Widget 对象。

其中组件又包括无状态组件和有状态组件。

  • 无状态组件(StatelessWidget)
    无状态组件,可以理解为将外部传入的数据转化为界面展示的内容,只会渲染一次。

  • 有状态组件(StatefulWidget)
    有状态组件,是定义交互逻辑和业务数据,可以理解为具有动态可交互的内容界面,会根据数据的变化进行多次渲染。

StatelessWidget 和 StatefulWidget 都是直接继承自 Widget 类,而这两个类也正是 Flutter 中非常重要的两个抽象类,它们引入了两种 Widget 模型。

2 两种Widget模型

2.1 StatelessWidget

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

StatelessWidget相对比较简单,它继承自Widget类,重写了createElement()方法。

StatelessElement 间接继承自Element类,与StatelessWidget相对应(作为其配置数据)。

StatelessWidget用于不需要维护状态的场景,并且只会被渲染一次,它通常在build方法中通过嵌套其它Widget来构建UI,在构建过程中会递归的构建其嵌套的Widget。

2.2 StatefulWidget

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

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

  @protected
  State createState();
}

和StatelessWidget一样,StatefulWidget也是继承自Widget类,并重写了createElement()方法,不同的是返回的Element 对象并不相同;另外StatefulWidget类中添加了一个新的接口createState()。

StatefulElement 间接继承自Element类,与StatefulWidget相对应(作为其配置数据)。StatefulElement中可能会多次调用createState()来创建状态(State)对象。

createState() 用于创建和StatefulWidget相关的状态,它在StatefulWidget的生命周期中可能会被多次调用。例如,当一个StatefulWidget同时插入到widget树的多个位置时,Flutter framework就会调用该方法为每一个位置生成一个独立的State实例,其实,本质上就是一个StatefulElement对应一个State实例。

2.2.1 State

一个StatefulWidget类会对应一个State类,State表示与其对应的StatefulWidget要维护的状态,State中的保存的状态信息可以:

  • 在widget 构建时可以被同步读取。
  • 在widget生命周期中可以被改变,当State被改变时,可以手动调用其setState()方法通知Flutter framework状态发生改变,Flutter framework在收到消息后,会重新调用其build方法重新构建widget树,从而达到更新UI的目的。

State中有两个常用属性:

  1. widget,它表示与该State实例关联的widget实例,由Flutter framework动态设置。注意,这种关联并非永久的,因为在应用生命周期中,UI树上的某一个节点的widget实例在重新构建时可能会变化,但State实例只会在第一次插入到树中时被创建,当在重新构建时,如果widget被修改了,Flutter framework会动态设置State.widget为新的widget实例。
  2. context,StatefulWidget对应的BuildContext,作用同StatelessWidget的BuildContext,表示当前widget在widget树中的上下文,每一个widget都会对应一个context对象(因为每一个widget都是widget树上的一个节点)。

3 生命周期

3.1 组件生命周期

Flutter 中说的生命周期,是独指有状态组件的生命周期,对于无状态组件生命周期只有一次 build 这个过程,也只会渲染一次。有状态组件的生命周期如下图:

Flutter-Widget生命周期.png

Flutter 中的生命周期,包含以下几个阶段:

  • createState ,该函数为 StatefulWidget 中创建 State 的方法,当 StatefulWidget 被调用时会立即执行 createState 。

  • initState ,该函数为 State 初始化调用,因此可以在此期间执行 State 各变量的初始赋值,同时也可以在此期间与服务端交互,获取服务端数据后调用 setState 来设置 State。

  • didChangeDependencies ,当State对象的依赖发生变化时会被调用;例如:在之前build() 中包含了一个InheritedWidget,然后在之后的build() 中InheritedWidget发生了变化,那么此时InheritedWidget的子widget的didChangeDependencies()回调都会被调用。典型的场景是当系统语言Locale或应用主题改变时,Flutter framework会通知widget调用此回调。

  • build ,主要是返回需要渲染的 Widget ,由于 build 会被调用多次,因此在该函数中只能做返回 Widget 相关逻辑,避免因为执行多次导致状态异常。在 build 之后还有个回调 addPostFrameCallback,在当前帧绘制完成后会回调,注册之后不能被解注册并且只会回调一次;addPostFrameCallback是 SchedulerBinding 的方法;由于 mixin WidgetsBinding on SchedulerBinding,所以添加这个回调有两种方式:SchedulerBinding.instance.addPostFrameCallback((_) => {});或者WidgetsBinding.instance.addPostFrameCallback((_) => {});

  • reassemble, 在 debug 模式下,每次热重载都会调用该函数,因此在 debug 阶段可以在此期间增加一些 debug 代码,来检查代码问题。

  • didUpdateWidget ,在widget重新构建时,Flutter framework会调用Widget.canUpdate来检测Widget树中同一位置的新旧节点,然后决定是否需要更新,如果Widget.canUpdate返回true则会调用此回调。正如之前所述,Widget.canUpdate会在新旧widget的key和runtimeType同时相等时会返回true,也就是说在在新旧widget的key和runtimeType同时相等时didUpdateWidget()就会被调用。父组件发生 build 的情况下,子组件该方法才会被调用,其次该方法调用之后一定会再调用本组件中的 build 方法。

  • deactivate ,在组件被移除节点后会被调用,如果该组件被移除节点,然后未被插入到其他节点时,则会继续调用 dispose 永久移除。

  • dispose ,永久移除组件,并释放组件资源。

Flutter 生命周期的整个过程可以分为四个阶段

  1. 初始化阶段:createState 和 initState
  2. 组件创建阶段:didChangeDependencies 和 build
  3. 触发组件 build:didChangeDependencies、setState 或者didUpdateWidget 都会引发的组件重新 build
  4. 组件销毁阶段:deactivate 和 dispose

3.2 组件首次加载过程

我们通过代码来看下组件首次加载执行的生命周期过程

在 lib 中创建 test_stateful_widget.dart

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';

class TestStatefulWidget extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    print('create state');
    return TestState();
  }
}

class TestState extends State<TestStatefulWidget> {
  /// 定义 state [count] 计算器
  int count = 1;

  /// 定义 state [name] 为当前描述字符串
  String name = 'test';

  @override
  void initState() {
    print('init state');
    super.initState();
  }

  @override
  void didChangeDependencies() {
    print('did change dependencies');
    super.didChangeDependencies();
  }

  @override
  void didUpdateWidget(TestStatefulWidget oldWidget) {
    count++;
    print('did update widget');
    super.didUpdateWidget(oldWidget);
  }

  @override
  void deactivate() {
    print('deactivate');
    super.deactivate();
  }

  @override
  void dispose() {
    print('dispose');
    super.dispose();
  }

  @override
  void reassemble() {
    print('reassemble');
    super.reassemble();
  }

  void changeName() {
    setState(() {
      print('set state');
      this.name = 'Flutter';
    });
  }

  @override
  Widget build(BuildContext context) {
    print('build');
    return Column(
      children: <Widget>[
        FlatButton(
            child: Text('$name $count'), onPressed: () => this.changeName())
      ],
    );
  }
}

在 main.dart 中加载该组件

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Test',
      theme: ThemeData(
          primaryColor: Colors.amberAccent
      ),
      home: Scaffold(
        appBar: AppBar(
          title: Text('生命周期测试'),
        ),
        body: Center(
          child: TestStatefulWidget(),
        ),
      ),
    );
  }
}

我们打开手机模拟器,然后运行该 App ,在输出控制台可以看到下面的运行打印日志信息:

I/flutter ( 7729): create state
I/flutter ( 7729): init state
I/flutter ( 7729): did change dependencies
I/flutter ( 7729): build

当我们点击 Android Studio 中的 Hot Reload 按钮(黄色小闪电 ⚡️)时控制台输出信息如下:


I/flutter ( 7729): reassemble
I/flutter ( 7729): did update widget
I/flutter ( 7729): build

3.3 触发组件再次 build

触发组件再次 build 有三种方式

  1. setState
  2. didChangeDependencies
  3. didUpdateWidget

setState的场景开发中你那个经常遇到,在数据状态变化时触发组件 build,在上面的代码运行后的界面中点击 test 文本按钮,然后观察控制台输出如下:

I/flutter ( 7729): set state
I/flutter ( 7729): build

didChangeDependencies场景:一般情况下我们会将一些比较基础的数据放到全局变量中,例如主题颜色、地区语言或者其他通用变量等。如果这些全局 state 发生状态变化则会触发该函数,而该函数之后就会触发 build 操作。

接下来看下didUpdateWidget 触发 build 的场景:
创建一个组件 SubStatefulWidget 继承 TestStatefulWidget

class SubStatefulWidget extends TestStatefulWidget {
  @override
  State<StatefulWidget> createState() {
    print('sub create state');
    return SubState();
  }
}

class SubState extends State<SubStatefulWidget> {

  @override
  void initState() {
    print('sub init state');
    super.initState();
  }

  @override
  void didChangeDependencies() {
    print('sub did change dependencies');
    super.didChangeDependencies();
  }

  @override
  void didUpdateWidget(TestStatefulWidget oldWidget) {
    print('sub did update widget');
    super.didUpdateWidget(oldWidget);
  }

  @override
  void deactivate() {
    print('sub deactivate');
    super.deactivate();
  }

  @override
  void dispose() {
    print('sub dispose');
    super.dispose();
  }

  @override
  void reassemble() {
    print('sub reassemble');
    super.reassemble();
  }

  void changeName() {
    setState(() {
      print('sub set state');
    });
  }

  @override
  Widget build(BuildContext context) {
    print('sub build');
    return Text('SubStatefulWidget');
  }
}

接着在 TestStatefulWidget 加载该组件

  @override
  Widget build(BuildContext context) {
    print('build');
    return Column(
      children: <Widget>[
        FlatButton(
            child: Text('$name $count'), onPressed: () => this.changeName()),
        SubStatefulWidget()
      ],
    );
  }

重新加载 APP,观看控制台输出如下:

I/flutter ( 8464): create state
I/flutter ( 8464): init state
I/flutter ( 8464): did change dependencies
I/flutter ( 8464): build
I/flutter ( 8464): sub create state
I/flutter ( 8464): sub init state
I/flutter ( 8464): sub did change dependencies
I/flutter ( 8464): sub build

上面日志先后打印了 TestStatefulWidget 与 SubStatefulWidget 四个状态函数 createState、initState、didChangeDependencies 和 build。当我们再次点击 test 文本按钮,观察控制台输出如下:

I/flutter ( 9425): set state
I/flutter ( 9425): build
I/flutter ( 9425): sub did update widget
I/flutter ( 9425): sub build

通过上面打印的日志我们可知,父组件调用setState不仅会触发自己 build,还会引发子组件重新 build ,虽然子组件没有任何改动。

3.4 触发组件销毁

在3.3的代码的基础上,我们直接在 TestStatefulWidget 组件中注释子组件 SubStatefulWidget 的调用

  @override
  Widget build(BuildContext context) {
    print('build');
    return Column(
      children: <Widget>[
        FlatButton(
            child: Text('$name $count'), onPressed: () => this.changeName()),
//        SubStatefulWidget()
      ],
    );
  }

然后点击 Android Studio 上的 Hot Reload 按钮观察控制台输出如下信息:

I/flutter ( 9425): reassemble
I/flutter ( 9425): sub reassemble
I/flutter ( 9425): did update widget
I/flutter ( 9425): build
I/flutter ( 9425): sub deactivate
I/flutter ( 9425): sub dispose

可以看到 SubStatefulWidget 被销毁

4 App 生命周期监听

在 Flutter 中,可以利用 WidgetsBindingObserver 类来监听App 生命周期

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() {}
}

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

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

当切换前、后台时,App 状态如下:

  • 从后台切入前台: AppLifecycleState.paused -> AppLifecycleState.inactive -> AppLifecycleState.resumed
  • 从前台退回后台:AppLifecycleState.resumed -> AppLifecycleState.inactive -> AppLifecycleState.paused

5 帧绘制回调

5.1 addPostFrameCallback

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

WidgetsBinding.instance.addPostFrameCallback((_){
  print(" 单次 Frame 绘制回调 ");
});

5.2 addPersistentFrameCallback

实时 Frame 绘制回调,会在每次绘制 Frame 结束后进行回调,可以用做 FPS 监测。

WidgetsBinding.instance.addPersistentFrameCallback((_){
  print(" 实时 Frame 绘制回调");
});

6 总结

  1. Flutter 中 Widget 分为两种:StatelessWidget 和 StatefulWidget
  2. Flutter 生命周期的整个过程可以分为四个阶段:初始化、组件创建、触发组件 build、组件销毁
  3. StatelessWidget 用于不需要维护状态的场景,只会 build 一次
  4. StatefulWidget 会被多次触发 build 函数,触发函数是setState、didChangeDependencies、didUpdateWidget
  5. 父组件调用setState不仅会触发自己 build,还会引发子组件重新 build ,虽然子组件没有任何改动
  6. 可以利用 WidgetsBindingObserver 类来监听App 生命周期
  7. 帧绘制回调有单次 Frame 绘制回调(addPostFrameCallback)和实时 Frame 绘制回调(addPersistentFrameCallback)
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349