Flutter 熟悉的陌生人Key 的原理和使用(wei.zhou小为)

每一个Widget都有一个可选传递的参数key,我们一般不会使用到它,但是在一些特定或者复杂的场景下,它必须出场,扮演着重要的角色,我们一起来学习认识一下它.

一.没有Key会发生什么事情

我们创建一个具有状态的Widget(Box)


class Box extends StatefulWidget {
  final Color? color;
  Box(this.color, {Key? key}) : super(key: key);

  @override
  _BoxState createState() => _BoxState();
}

class _BoxState extends State<Box> {
  int count = 0;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: (){
        setState(() {
          count++;
        });
      },
      child: Container(
          width: 150,
          height: 150,
          color: widget.color,
          child: Center(
            child: Text('$count',style: const TextStyle(fontSize: 50),),
          )),
    );
  }
}

大家可以看到,里面的数字是在state里面,我们手指点击数字就会加一。 然后现在看看主程序的代码:

class _KeyShareDemoState extends State<KeyShareDemo> {

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title ?? ''),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Box(Colors.red),
            Box(Colors.yellow),
            Box(Colors.blue),
          ],
        ),
      ),
    );
  }
}

初始状态:

我们放在同一层级的Column下面,然后第一个Box点击一下,第二个Box点击两下,第三个Box点击三下,运行结果如下图

截屏2022-06-19 下午12.22.40.png

颜色顺序为红、黄、蓝,数字分别为1、2、3.

情景一

现在,我们人为的将第一个Widget和第三个Widget的代码顺序进行对调,我们猜想:此时的颜色和数字都会进行对调,是吗?结果如下图:

Simulator Screen Shot - iPhone 13 - 2022-06-19 at 12.32.12.png

我们发现:颜色是对调了,但是里面的数字还是原来的数字,令人费解.

情景二:

我们回到初始化的状态,还是红、黄、蓝的三个按钮,数字还是1、2、3,我们现在人为的去掉第一个红色的Widget,那么我们猜想是不是应该留下数字2和3,数字1 不见了,颜色剩下黄色和蓝色呢?运行结果如下.

Simulator Screen Shot - iPhone 13 - 2022-06-19 at 12.57.34.png

我们发现,颜色每次都是对的,没有让我们失望,但是这个数字1居然留下了,数字3不见了,我们更加疑惑了.

这个时候系统其实已经分不清楚谁是谁了,因为它们处于同一层级,又是相同的类型。所以,我们需要给它们一个Key作为它们各自的唯一标识符.如下图:

截屏2022-06-19 下午3.25.31.png

我们给了它们每个人一个ValueKey,并且每一个都给了不同的值,我们就发现,无论我们是调换顺序也好,还是删除其中一个Widget也好,它们都会如我们所愿,不会乱了.

结论:在同一层级树中(注意不同层级另当别论,这里的同一层级指的是三个Box都在同一个Column下面),Key可以作为唯一标识,标记我们的Widget,系统在复用的时候才不会混乱.

情景三

我们现在有Key了,都是ValueKey.我在第三个Box外面套上一层Center这个小部件,依然保留ValueKey.如下图:

截屏2022-06-19 下午3.40.55.png

这样加上了Center过后,然后点击热更新,事情再次没有如我们所愿,我们的第三个Box,刚开始是3,现在变成了0.但是颜色依然没有问题!

Simulator Screen Shot - iPhone 13 - 2022-06-19 at 15.49.31.png

情景四

我这里就不进行演示了,大家可以去进行尝试,在同一层级下的Container和Text,你调换位置和删除,都会如你所愿,不会混乱。因为它们都是StatelessWidget,他们没有状态state.我们在几次的试验中也发现,颜色处于widget侧,它始终是没有问题的,出问题的是状态。

二.Widget树与Element树的对应关系

为了解释上面的奇异现象,我们必须去了解与学习Widget树与Element树的原理与关系。如下图所示:

截屏2022-06-19 下午7.06.03.png

1.Widget树:我们平时写代码的地方,它是一个蓝图,是一份描述UI元素Element的配置的描述文件.

2.Element树:真正生成视图对象和状态的树。它与widget是一一对应的关系,每个widget都会调用其内部的 createElement方法,从而实例化生成Element对象。如下图,为Widget的源码.

截屏2022-06-19 下午4.22.01.png

3.State是跟着Element对象走的,并不在Widget侧。Widget负责如何渲染,比如颜色,大小,形状等等,而Element负责管理里面的状态。所以状态是随着Element来改变的,外观是随着Widget改变的.二者是分开的。

4.Widget和Element为什么要分开呢? 因为Widget是不可变的!可以改变的是State状态,当状态发生改变后,flutter会去重建一个新的widget,去替换掉旧的widget,而不是去改变这个widget,因为widget是不可变的.

5.我们在上面的例子中,交换两个Box的位置,或者去掉一个Box时,它们所对应的Element并不一定被调换了顺序,或者说被正确的删除了。这才是问题的关键所在.如图所示:

截屏2022-06-19 下午6.24.33.png

这是一个Widget里面的源码方法,canUpdate方法会对新旧widget的类型和key进行判断,如果它们都相等,系统就认为它们可以更新。

意思是:一旦这个方法判定成立,Widget与Element对象就可以进行对应!可以进行关联!那么一旦判断错误,该Widget就会错误的与Element和State进行关联。上面我举出的例子,就是发生了这种情况!

三.复盘解释上面的现象

1.widget顺序交换问题.

无key:

当我们调换顺序时,系统重走canUpdate方法进行判定,由于在同一层级下,全部都是Box类型的Widget,并且我们没有给到key,key都为null,null==null。那么,此判断条件就成立了,返回true。也就是说,以前的Element1就关联上了交换顺序后的widget3,由于Element1的状态为1,所以第一个方块还是显示的1.同理可得,由于我们没有给Key,以前的Element3就关联上了交换顺序后的widget1,Element3的状态为3,那么第三个方块还是显示的3.由此可见,虽然我们交换了widget的顺序,但是Element的顺序并不一定会跟着发生改变,一切以canUpdate判定结果为准.

有key:

比如现在我们交换的是Box2与Box3的顺序,系统在检索Box3与Element2的时候,由于我们给了Key,那么canUpdate方法将返回false,他们将不会建立联系,如图所示:

截屏2022-06-19 下午7.46.19.png

不能建立对应联系的话,它会继续进行同级别检索(注意这里我还是说的同级,跨级我后面会说明),同级检索到可以建立对应联系的Box2后,它们就会建立正确的对应关系了,如图:

截屏2022-06-19 下午7.54.02.png

到了最后,每个widget与Element都建立了正确的对应关系,那么对应的状态也跟着走了,数字也正确了,这样也就达到了我们的目的.如图所示:

截屏2022-06-19 下午7.57.39.png

2.例子中删除widget的问题

有了上面的详细解释,这个由一张图概括:

截屏2022-06-19 下午8.06.00.png

我们删除掉第一个widget后,由于我们没有给Key,根据canUpdate方法的判定,Box2会与之前的Element1对应,Box3与之前的Element2对应,所以数字1和2被保留。而Element3由于在同级中没有检索到能与之匹配的Widget,那么Element3对象和State都会一并没销毁.

3.例子中有给Key,套上Center导致的问题.

可能你也发现了,开始的类型是Box,我们虽然给了Key,但是我们套上Center过后,这个类型就变了。canUpdate类型判断就不会成功。那么ELement3由于在同层级无法对应,就会被销毁,系统会重新生成新的与Center对应的Element对象,那么状态也被重置了。所以数字就是初始化的状态0.

4.StatelessWidget不需要使用Key?

我们开发中常见的Container、Text都是StatelessWidget,它是没有状态State的。比如在一个Column中,我们写两个Container,我们不传入key。现在我们交换他们的位置,系统会只比较它们的 runtimeType。这里 runtimeType 一致,canUpdate 方法返回 true,两个 Widget 被交换了位置,但是此时这两个 Element 将不会交换位置,Element调用新持有Widget的build方法重新构建,所以widget得到更改,位置交换。UI的内容,都在widget侧,所以在同一层级树中,不需要使用Key.

三.几种Key的介绍使用.

我们主要有两大类的Key需要了解.分为局部键与全局键。

  • LocalKey 局部键,在同一级中要唯一,可以理解为同级唯一性
  • GlobalKey 全局键 , 在整个App中必须是唯一的.

局部键的性能是比全局键快的多,因为它只是在同级中搜索比较,全局键在整个工程中都是唯一的,所以它更慢,当然它更加的强大.

截屏2022-06-19 下午9.18.18.png

局部键:

刚刚我们演示的代码都在一个Colunm之中,他们处于同一层级,那么我们就使用的局部键中的ValueKey。

同一层级中,键必须是唯一的,不然系统就会报错崩溃。比如我们的Column、Row、Stack、TabarView,它们的子部件都是同一层级,如果程序出现异常,你就要进行思考是否要使用Key了。

同一层级,使用相同类型的Widget,这个时候子Widget的状态数据需要更新(比如搜索功能),这是Key的一个实战使用的场景

1.ValueKey : 我们可以看到,里面是一个范型.我们传什么都可以,但是注意,它比较的是值!它的使用,我们在上面的例子中已经展示.

截屏2022-06-19 下午9.43.07.png

2.ObjectKey : 它和ValueKey的区别就在于,它比较的是对象,而不是值.如图:

WeChatd0ea6b176086851260590b118b457d0e.png

可以看到它比较的是Object了,不再是一个范型了。这是这两个Key唯一的区别.

那么我们现在定义一个叫做People的对象出来,如下所示.

class People{
  final String name;
  final int age;

  People(this.name, this.age);

  @override
  bool operator ==(Object other) =>
      identical(this, other) ||
      other is People && runtimeType == other.runtimeType && name == other.name && age == other.age;

  @override
  int get hashCode => name.hashCode ^ age.hashCode;
}

我现在把它用到ValueKey上面,我故意把它们的值定义为相同的,这样子系统就可以报错出来,因为同一层级不能使用相同的Key。如图所示,系统果然在提示我说,你使用了重复的Key,那么这两个ValueKey就是想等的.

WechatIMG160.jpeg

我现在使用ObjectKey,程序运行正常,没有报错,系统不认为它们两相等,因为它在比较两个对象是否相等.

截屏2022-06-19 下午10.47.50.png

3.UniqueKey

总结:我们前面提到的ValueKey和ObjectKey,还有我们后面即将介绍的GlobalKey,它们有一个共同点就是,它们会帮助我们保持状态State,与其说是保持状态,还不如说是保持了Element对象(因为State是跟着Element走的嘛),因为这些Key自己不会变,所以在canUpdate方法判定中,能够判断成功,判定成功的话,Element就能够与Widget对应上来,所以状态State就被保留下来。

但是UniqueKey不一样,它是与自身进行比较,并且它每一次都不一样,它自己会变.每一次的UniqueKey()和UniqueKey()它是不相等的。每一次刷新页面,它都是独一无二的,那么canUpdate方法永远都不会判定成功,widget与Element永远不会对上,Element对象每次都会被重新创建,与之一体的State也一同被重新构建,状态会被重置!每次都会被重置!换句话说就是:我们在刷新页面的时候,UniqueKey主动为我们丢失了状态, 让状态回到原点.

WechatIMG161.jpeg

4.GlobalKey

1.全局键,它依然可以为我们保持状态State,与局部键不同的是,它可以为我们跨越层级的保持widget的状态。因为它是在整个App的树中进行索引查找的,所以它的速度更慢,但是也正因如此,可以实现跨层级保持ELement.

final GlobalKey _globalKey= GlobalKey();//不同其他的Key,它需要进行初始化被持有.

WechatIMG162.jpeg

2.GlobalKey为我们提供了几个主要的方法,让我们既可以访问到Widget树的东西,也可以访问到Render树的东西,当然也可以访问到Element树的东西.

2.1 获取对应的widget

onPressed: () {
  final widget = _globalKey.currentWidget as Box;
  print(widget.color);
}

此处的Widget类型是Box类型,所以需要转换一下,我们当时在Box的Widget侧定义了一个color属性,我们就可以通过这种方式访问到它.

2.2 获取对应的context和RenderObject

context其实就是Element呀,通过它也可以访问到RenderObject。

final renderBox = _globalKey.currentContext!.findRenderObject() as RenderBox;

我们通过RenderBox就可以获取到它的尺寸和坐标了.

2.3 获取对应的State状态

我们定义一个GlobalKey,我们顺带可以给上它需要定位的state状态类型为_BoxState类型。

截屏2022-06-27 下午10.55.18.png

如图所示,我们点击三次过后,状态数字为3.可以通过GlobalKey获取到_BoxState,并打印出该count值

1871656342276_.pic.jpg

四.什么时候会使用Key?业务场景是什么

  • 搜索功能 : 可能用到,当搜索不通内容的时候,给出不同的结果,你可能需要改变状态,重新构建Widget等, 这个需要看你的需求和实现方式.
  • ValueKey:对列表ListView中item进行滑动删除、调换顺序、增加等的时候需要用到。
  • ObjectKey:如果你有一个电话本应用,它可以记录某个人的电话号码,并用列表显示出来,同样的还是需要有一个滑动删除操作.我们知道人名可能会重复,这时候你无法保证给 Key 的值每次都会不同。但是,当人名和电话号码组合起来的 Object 将具有唯一性

好了,以上!

猜你喜欢

转载自juejin.im/post/7110983415528161310
今日推荐