Flutter Widget 生命周期 & key探究

Widget

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

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

两种Widget模型

StatelessWidget

StatelessWidget用于不需要维护状态的场景,其对应的Element是StatelessElement
在这里插入图片描述
在这里插入图片描述

StatefulWidget

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

生命周期

理论

Flutter 中说的生命周期,是独指有状态组件(StatefulWidget)的生命周期,对于无状态组件生命周期只有一次 build 这个过程,也只会渲染一次,StatefulWidget生命周期图如下:
在这里插入图片描述
Flutter 中的生命周期,包含以下几个阶段:

  • createState :该函数为 StatefulWidget 中创建 State 的方法,当 StatefulWidget 被调用时会立即执行 createState 。
  • initState :该函数为 State 初始化调用,紧接着createState之后调用,可以在此期间执行 State 各变量的初始赋值,同时也可以在此期间与服务端交互
  • didChangeDependencies :第一种情况是StatefulElement mount时会回调,这种情况会紧跟initState被回调
    还有一种情况是当State对象的“依赖”发生变化时会被调用,这种依赖是指通过context.dependOnInheritedWidgetOfExactType进行的依赖
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
  • build :主要是返回需要渲染的 Widget ,由于 build 会被调用多次,因此在该函数中只能做返回 Widget 相关逻辑
  • 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 didUpdateWidget 和 build
  3. 组件销毁阶段:deactivate 和 dispose

实例

class LifeCycleTest extends StatefulWidget {
  final String TAG = "LifeCycleTest";

  
  State<StatefulWidget> createState() {
    print('$TAG createState');
    return LifeCycleTestState();
  }
}

class LifeCycleTestState extends State<LifeCycleTest> {
  
  void initState() {
    print('${widget.TAG} initState');
    super.initState();
  }

  
  void reassemble() {
    print('${widget.TAG} reassemble');
    super.reassemble();
  }

  
  void didChangeDependencies() {
    print('${widget.TAG} didChangeDependencies');
    super.didChangeDependencies();
  }

  
  void didUpdateWidget(covariant LifeCycleTest oldWidget) {
    print('${widget.TAG} didUpdateWidget');
    super.didUpdateWidget(oldWidget);
  }

  
  void deactivate() {
    print('${widget.TAG} deactivate');
    super.deactivate();
  }

  
  void dispose() {
    print('${widget.TAG} dispose');
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    print('${widget.TAG} build');
    return Scaffold(
      body: SafeArea(
        child: Column(
          children: [
            GestureDetector(
              onTap: () {
                setState(() {});
              },
              child: Container(
                width: 100,
                height: 100,
                color: Colors.red,
              ),
            ),
            MyInheritedWidget(LifeCycleTestChild(), 20)
          ],
        ),
      ),
    );
  }
}

class LifeCycleTestChild extends StatefulWidget {
  final String TAG = "LifeCycleTestChild";

  
  State<StatefulWidget> createState() {
    print('$TAG createState');
    return LifeCycleTestChildState();
  }
}

class LifeCycleTestChildState extends State<LifeCycleTestChild> {
  
  void initState() {
    print('${widget.TAG} initState');
    super.initState();
  }

  
  void reassemble() {
    print('${widget.TAG} reassemble');
    super.reassemble();
  }

  
  void didChangeDependencies() {
    print('${widget.TAG} didChangeDependencies');
    super.didChangeDependencies();
  }

  
  void didUpdateWidget(covariant LifeCycleTestChild oldWidget) {
    print('${widget.TAG} didUpdateWidget');
    super.didUpdateWidget(oldWidget);
  }

  
  void deactivate() {
    print('${widget.TAG} deactivate');
    super.deactivate();
  }

  
  void dispose() {
    print('${widget.TAG} dispose');
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    print('${widget.TAG} build');
    var dependOnInheritedWidgetOfExactType =
        context.dependOnInheritedWidgetOfExactType<MyInheritedWidget>();
    return Container(
      child: Text("${(dependOnInheritedWidgetOfExactType as MyInheritedWidget).count}"),
    );
  }
}

class MyInheritedWidget extends InheritedWidget {
  final int count;

  MyInheritedWidget(
    Widget child,
    this.count,
  ) : super(child: child);

  
  bool updateShouldNotify(covariant InheritedWidget oldWidget) {
    return true;
  }
}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
上面图说明了几个点

  • 第二个图:didChangeDependencies在widget第一次初始化的时候都会调用
  • 第二个图:LifeCycleTest组件发生build,LifeCycleTestChild子组件调用didUpdateWidget,自身并没有调用didUpdateWidget,第三个图,热加载后都调用了didUpdateWidget,说明了父组件发生 build 的情况下,子组件该方法才会被调用
  • 第三个图:热加载后,只有LifeCycleTestChild调用了didChangeDependencies,说明通过context.dependOnInheritedWidgetOfExactType进行的依赖会调用该方法

Getx生命周期

先看下Controller的集成层级
在这里插入图片描述
再看下对应类的定义
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
其中 GetxController只是有个 update 方法用于通知组件刷新。
在 DisposableInterface 中覆盖了onInit 方法,实际多干了一件事,就是监听第一帧回调,等第一帧回调过来之后再调用onReady
然后我们再看下这些生命周期分别是在什么时候调用的
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  • onInit:组件在内存分配后会被马上调用,可以在这个方法对 controller 做一些初始化工作。
  • onReady:在 onInit 一帧后被调用,适合做一些导航进入的事件,例如对话框提示、SnackBar 或异步网络请求。
  • onClose:在 onDelete 方法前调用、用于销毁 controller 使用的资源,例如关闭事件监听,关闭流对象,或者销毁可能造成内存泄露的对象,例如 TextEditingController,AniamtionController。也适用于将数据进行离线持久化。
    [图片]
    所以有了 GetxController 的生命周期后,我们就可以完全替换掉 StatefulWidget 了。
    onInit 或 onReady替换 initState
    onClose 替换 dispose,比如关闭流

Key

key的作用是:控制Element树上的Element是否被复用
如果两个widget的runtimeType和key相等(用==比较),那么原本指向旧widge的element,它的指针会指向新的widget上(通过Element.update方法)。如果不相等,那么旧element会从树上移除,根据当前新的widget重新构建新element,并加到树上指向新widget。
在这里插入图片描述
在这里插入图片描述
基于Element的复用机制的解释

在Flutter中,Widget是不可变的,它仅仅作为配置信息的载体而存在,并且任何配置或者状态的更改都会导致Widget的销毁和重建,但好在Widget本身是非常轻量级的,因此实际耗费的性能很小。与之相反,RenderObject就不一样了,实例化一个RenderObject的成本是非常高的,频繁地实例化和销毁RenderObject对性能的影响非常大,因此为了高性能地构建用户界面,Flutter使用Element的复用机制来尽可能地减少RenderObject的频繁创建和销毁。当Widget改变的时候,Element会通过组件类型以及对应的Key来判断旧的Widget和新的Widget是否一致:

1、如果某一个位置的旧Widget和新Widget不一致,就会重新创建Element,重建Element的同时也重建了RenderObject;
2、如果某一个位置的旧Widget和新Widget一致,只是配置发生了变化,比如组件的颜色变了,此时Element就会被复用,而只需要修改Widget对应的Element的RenderObject中的颜色设置即可,无需再进行十分耗性能的RenderObject的重建工作。
在这里插入图片描述

在这里插入图片描述

分类

flutter 中的key总的来说分为以下两种:

  • 局部键(LocalKey):ValueKey、ObjectKey、UniqueKey
  • 全局键(GlobalKey):GlobalObjectKey

ValueKey

ValueKey是通过某个具体的Value值来做区分的Key,如下:

key:ValueKey(1),
key:ValueKey("2"),
key:ValueKey(true),
key:ValueKey(0.1),
key:ValueKey(Person()), // 自定义类实例

可以看到,ValueKey的值可以是任意类型,甚至可以是我们自定义的类的实例。判断2个ValueKey是否相等是根据里面的value是否来判断的,如果value是自定义类,则可以通过重写自定义类的操作符来实现

例如,现在有一个展示所有学生信息的ListView列表,每一项itemWidget所对应的学生对象均包含某个唯一的属性,例如学号、身份证号等,那么这个时候就可以使用ValueKey,其值就是对应的学号或者身份证号。

在这里插入图片描述

ObjectKey

ObjectKey的使用场景如下:
现有一个所有学生信息的ListView列表,每一项itemWidget对应的学生对象不存在某个唯一的属性(比如学号、身份证号),任一属性均有可能与另外一名学生重复,只有多个属性组合起来才能唯一的定位到某个学生,那么此时使用ObjectKey就最合适不过了。

ObjectKey判断两个Key是否相同的依据是:两个对象是否具有相同的内存地址,不论自定义对象是否重写了==运算符判断,均会被视为不同的Key

在这里插入图片描述

UniqueKey

顾名思义,UniqueKey是一个唯一键,不需要参数,并且每一次刷新都会生成一个新的Key。
一旦使用UniqueKey那么就不存在Element复用了

GlobalKey

GlobalKey是全局唯一的键,一般而言,GlobalKey有如下几种用途:

  • 获取配置、状态以及组件Element
    • _globalKey.currentWidget:获取当前组件的配置信息(存在widget树中)
    • _globalKey.currentState:获取当前组件的状态信息(存在Element树中)
    • _globalKey.currentContext:获取当前组件的Element
  • 实现组件的局部刷新
    将需要单独刷新的widget从复杂的布局中抽离出去,然后通过传GlobalKey引用,这样就可以通过GlobalKey实现跨组件的刷新了。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

key作用示例

一般情况下我们不使用key,程序也是能正常运行的,只有部分特殊情况下需要使用key,下面我们看一个例子

import 'dart:math';

import 'package:flutter/material.dart';

class PositionedTiles extends StatefulWidget {
  
  State<StatefulWidget> createState() => PositionedTilesState();
}

class PositionedTilesState extends State<PositionedTiles> {
  late List<Widget> tiles;

  
  void initState() {
    super.initState();
    tiles = [
      // StatefulColorfulTile(),
      // StatefulColorfulTile(),
      // StatefulColorfulTile(key: UniqueKey()),
      // StatefulColorfulTile(key: UniqueKey()),
      StatelessColorfulTile(),
      StatelessColorfulTile(),
    ];
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: Column(
          children: tiles,
        ),
      ),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.sentiment_very_satisfied),
        // child: Icon(Icons.sentiment_very_dissatisfied),
        onPressed: swapTiles,
      ),
    );
  }

  void swapTiles() {
    setState(() {
      tiles.insert(1, tiles.removeAt(0));
    });
  }
}

// ignore: must_be_immutable
class StatelessColorfulTile extends StatelessWidget {
  Color color = ColorUtil.randomColor();  //color属性直接在widget中

  
  Widget build(BuildContext context) {
    return Container(color: color, child: Padding(padding: EdgeInsets.all(70.0)));
  }
}

class StatefulColorfulTile extends StatefulWidget {
  StatefulColorfulTile({Key? key}) : super(key: key);

  
  State<StatefulWidget> createState() => StatefulColorfulTileState();
}

class StatefulColorfulTileState extends State<StatefulColorfulTile> {
  Color? color; //color属性在State中

  
  void initState() {
    super.initState();
    color = ColorUtil.randomColor();
    print('initState');
  }

  
  Widget build(BuildContext context) {
    return Container(color: color, child: Padding(padding: EdgeInsets.all(70.0)));
  }
}

class ColorUtil {
  static Color randomColor() {
    var red = Random.secure().nextInt(255);
    var greed = Random.secure().nextInt(255);
    var blue = Random.secure().nextInt(255);
    return Color.fromARGB(255, red, greed, blue);
  }
}

上面的代码效果如下,可以看到使用StatelessColorfulTile时,点击按钮后两个色块能成功交换:

当我们把代码换成下面这样
在这里插入图片描述
神奇的事情发生了,点击交换按钮没有任何反应
那在使用StatefulColorfulTile的前提下,如何让色块再次点击按钮后能发生交换呢?我猜聪明的你已经想到了,就是设置key属性,即把代码改成下面这个样子
在这里插入图片描述
下面我们来解释下为什么会出现这样的结果:

  1. 为什么StatelessWidget的能交换
    在这里插入图片描述
    在这里插入图片描述
    当代码调用PositionedTiles.setState交换两个Widget后,flutter会从上到下逐一对比Widget树和Element树中的每个节点,如果发现节点的runtimeType和key一致的话(这里没有key,因此只对比runtimeType),那么就认为该Element仍然是有效的,可用复用,于是只需要更改Element的指针,就可以直接复用
    对于StatelessWidget中的color信息是直接在widget中的,那widget重新build直接就更新了颜色

  2. 为啥StatefulColorfulTile要加key才能交换
    StatefulWidget的color属性是放在State中的,我们上面说过State被Element管理
    我们先看下不带key时的树结构

首先还是Widget更新后,flutter会根据runtimeType和key比较Widget从而判断是否需要重新构建Element,这里key为空,只比较runtimeType,比较结果必然相等,所以Element直接复用。
StatefulColorfulTile在重新渲染时,Color属性不再是从Widget对象(即自身)里获取,而是从Element的State里面获取,而Element根本没发生变化,所以取到的Color也没有变化,最终就算怎么渲染,颜色都是不变的,视觉效果上也就是两个色块没有交换了。
接着看有了key之后的树结构

交换前:
在这里插入图片描述

交换后,发现两边key不相等,于是尝试在Element 列表里面查找是否还有相同的key的Element,发现有,于是重新排列Element让相同key的配对
在这里插入图片描述
rebuild后,Element已交换,重新渲染后视觉上就看到两个色块交换位置了:
在这里插入图片描述
在这种加了key又交换位置的情况下,Element和widget都是直接复用的,所以点击交换位置,widget没有触发build方法,原因在于canUpdate方法返回false,didUpdateWidget也没有回调,build方法也不会被触发

接下来我们在原来的demo上做些小改动,在要交换的2个Widget外面分别套上Padding,我们看下效果:
在这里插入图片描述
我们发现每次点击交换位置,2个Widget都变成了新的颜色,即两个 Widget 的 Element 并不是交换顺序,而是被重新创建了

当交换子节点的位置时,Flutter 的 element-to-widget 匹配逻辑一次只会检查树的一个层级。
在Column这一层级,padding 部分的 runtimeType 并没有改变,且不存在 Key。Element复用,然后再比较下一个层级。由于内部的 StatefulColorfulTile 存在 key,且现在的层级在 padding 内部,该层级没有多子 Widget。canUpdate 返回 flase,Flutter 将会认为这个 Element 需要被替换。然后重新生成一个新的 Element 对象装载到 Element 树上替换掉之前的 Element。第二个 Widget 同理。

所以为了解决这个问题,我们需要将 key 放到 Padding 的 这一层级就可以了

根据上面的例子我们能了解到:如果要在有状态的、类型相同、同一层级的 widget 集合上进行添加、删除、排序等操作,可能需要使用到 key。

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值