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 生命周期的整个过程可以分为四个阶段
- 初始化阶段:createState 和 initState
- 组件创建阶段:didChangeDependencies didUpdateWidget 和 build
- 组件销毁阶段: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属性,即把代码改成下面这个样子
下面我们来解释下为什么会出现这样的结果:
-
为什么StatelessWidget的能交换
当代码调用PositionedTiles.setState交换两个Widget后,flutter会从上到下逐一对比Widget树和Element树中的每个节点,如果发现节点的runtimeType和key一致的话(这里没有key,因此只对比runtimeType),那么就认为该Element仍然是有效的,可用复用,于是只需要更改Element的指针,就可以直接复用
对于StatelessWidget中的color信息是直接在widget中的,那widget重新build直接就更新了颜色 -
为啥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 的 这一层级就可以了文章来源:https://www.toymoban.com/news/detail-480888.html
根据上面的例子我们能了解到:如果要在有状态的、类型相同、同一层级的 widget 集合上进行添加、删除、排序等操作,可能需要使用到 key。文章来源地址https://www.toymoban.com/news/detail-480888.html
到了这里,关于Flutter Widget 生命周期 & key探究的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!