Flutter各种Key到底是干嘛的?

Flutter开发中经常碰到一种现象,当我们调用setState更新状态时,状态会莫名其妙的丢失。本文详细介绍Flutter中Key的原理及其能解决的问题。

前言

Flutter开发中经常碰到一种现象,当我们调用setState更新状态时,状态会莫名其妙的丢失,比如下面的例子:

首先定义一个有状态的盒子,盒子显示一个随机数字

class Box extends StatefulWidget {
  final Color color;
​
  const Box(this.color, {Key? key}) : super(key: key);
​
  @override
  State<Box> createState() => _BoxState();
}
​
class _BoxState extends State<Box> {
  var num = Random().nextInt(100);
​
  @override
  Widget build(BuildContext context) {
    return Container(
      width: 100,
      height: 100,
      alignment: Alignment.center,
      color: widget.color,
      child: Text(num.toString()),
    );
  }
}

显示一个绿色盒子和一个蓝色盒子,点击按钮将蓝色盒子放到前面

class _HomeViewState extends State<HomeView> {
  var widgets = <Widget>[
    Container(color: Colors.green),
    Container(color: Colors.blue),
  ];
​
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Row(
        children: widgets,
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _switchBox(),
      ),
    );
  }
​
  _switchBox() {
    widgets.insert(0, widgets.removeAt(1));
    setState(() {});
  }
}

flutter不加key显示问题

从图中我们看到蓝色盒子确实移动到前面,但是盒子中显示的数据却出现了问题。

产生这个问题的原因我们从源码分析:

我们都知道widget是有一个element一一对应的,element才是真正展示到屏幕的东西。而StatefulElement在创建时会创建保存一个State。

//StatefulElement只有在重新创建的时候才创建一个新的State
class StatefulElement extends ComponentElement {
     StatefulElement(StatefulWidget widget)
      : _state = widget.createState(),
        super(widget)
}

Framework会根据 Widget.canUpdate 来判断新widget能否可以作为新的配置直接更新Element。

static bool canUpdate(Widget oldWidget, Widget newWidget) {
  //从这里也能看出来widget其实只是一个配置文件,我们只关注它的类型
  return oldWidget.runtimeType == newWidget.runtimeType
      && oldWidget.key == newWidget.key;
}

当 canUpdate 方法返回true时(widget类型和key都匹配的情况)直接更新Element,上面的现象其实就是直接更新了Element,Element的位置并没有发生改变,所以还是显示原先的状态,只是把widget替换为新的widget,所以颜色改变了状态没变。

没加key,直接更新Element,Element树如下图:

flutter不加key分析图

要解决这个问题就需要让 canUpdate 方法返回 false,因为上面例子类型都是Box,这个时候就需要我们的Key其作用了,我们给蓝盒子设置一个Key,当把蓝盒子放到0的位置,新的widget和旧的widget的key是不一致的,所以不会直接更新Element,而是去寻找具有相同key的Element放到0的位置。

key相当于身份证,Element可以根据key去和widget匹配,并把Element放到和widget相对应的位置上。当然了,如果找不到key对应的的Element,就需要重新创建一个新的Element。

加入key之后的Element树如下:

flutter加key分析图

Key有两种类型GlobalKey和LocalKey,当我们设置的是GlobalKey时,Framework会从整个Element树中查找相同key的Element复用,当我们设置的是LocalKey时,Framework会从同级Element中查找相同的key的Element复用。

LocalKey又有三个子类:ValueKey、ObjectKey、UniqueKey,下面说一下各种Key的基本用法。

GlobalKey

GlobalKey主要有两个作用,一个就是解决Element复用的问题,另一个是用来获取widget对应的Element。

class _HomeViewState extends State<HomeView> {
  var widgets = <Widget>[
    Container(color: Colors.green),
    //给蓝色盒子设置一个GlobalKey
    Container(color: Colors.blue,key: GlobalKey(),),
  ];
​
 ...
​
  _switchBox() {
    var blueBox = widgets.removeAt(1);
    GlobalKey key = blueBox.key as GlobalKey;
    //获取Element(类似document.findElementById)
    var blueBoxElement = key.currentContext;
    //把蓝色盒子放到Center里面,依然可以通过GlobalKey复用Element
    widgets.insert(0, Center(child: blueBox));
    setState(() {});
  }
}

ValueKey

看源码我们知道ValueKey是用 == 来匹配是否相等,这个 == 就是Java中的equals,可以重写 == 方法。

//ValueKey.class
@override
  bool operator ==(Object other) {
    if (other.runtimeType != runtimeType)
      return false;
    return other is ValueKey<T>
        && other.value == value;
  }

举例:

class _HomeViewState extends State<HomeView> {
  var widgets = <Widget>[
    Container(color: Colors.green),
    //给蓝色盒子设置一个ValueKey
    Container(color: Colors.blue,key: const ValueKey("blue")),
  ];
​
 ...
​
  _switchBox() {
    widgets.insert(0, widgets.removeAt(1));
    setState(() {});
  }
}

ObjectKey

看源码我们知道ObjectKey是用 identical 来匹配是否相等,这个 identical 就是Java中的 == 。

//ObjectKey.class
@override
  bool operator ==(Object other) {
    if (other.runtimeType != runtimeType)
      return false;
    return other is ObjectKey
        && identical(other.value, value);
  }

使用方式和ValueKey类似。

UniqueKey

看源码没有重新 == 方法,也没有传值,所以是一个独一无二的Key。

举例:

class _HomeViewState extends State<HomeView> {
  var textNum = 1;
​
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: AnimatedSwitcher(
        duration: const Duration(seconds: 1),
        //每次build都会重新一个UniqueKey(),element才会重建,才会显示切换动画
        child: Text(textNum.toString(),key: UniqueKey(),),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _increaseNum(),
      ),
    );
  }
​
  _increaseNum() {
    setState(() {
      ++textNum;
    });
  }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值