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(() {}); } }
从图中我们看到蓝色盒子确实移动到前面,但是盒子中显示的数据却出现了问题。
产生这个问题的原因我们从源码分析:
我们都知道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树如下图:
要解决这个问题就需要让 canUpdate 方法返回 false,因为上面例子类型都是Box,这个时候就需要我们的Key其作用了,我们给蓝盒子设置一个Key,当把蓝盒子放到0的位置,新的widget和旧的widget的key是不一致的,所以不会直接更新Element,而是去寻找具有相同key的Element放到0的位置。
key相当于身份证,Element可以根据key去和widget匹配,并把Element放到和widget相对应的位置上。当然了,如果找不到key对应的的Element,就需要重新创建一个新的Element。
加入key之后的Element树如下:
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; }); } }