Flutter-认识各种Key及使用

在开发中会发现每当我们创建一个小部件时,都有一个参数 key,这个key到底有什么作用呢?下面就来介绍下key是什么,有什么作用以及如何使用。

key是什么

在 Flutter 开发中我们经常与要状态打交道。我们知道 WidgetStatefulStateless 两种。Key 能够帮助开发者在 Widget 树中保存状态,在一般的情况下,我们并不需要使用 Key。那究竟什么时候应该使用 Key 呢?

下面举个例子来说明:

  • 创建了一个100*100的盒子,颜色是随机生成的,显示的文字是外面传进来的
class StfulItem extends StatefulWidget {
  final String title;
  const StfulItem(this.title, {Key? key}) : super(key: key);
  @override
  _StfulItemState createState() => _StfulItemState();
}

class _StfulItemState extends State<StfulItem> {
  //随机生成颜色
  final color = Color.fromRGBO(
      Random().nextInt(256), Random().nextInt(256), Random().nextInt(256), 1.0);
  @override
  Widget build(BuildContext context) {
    return Container(
      width: 100,
      height: 100,
      color: color,
      child: Text(widget.title),
    );
  }
}
复制代码
  • 创建了三个上面创建的盒子,然后点击按钮时删除第一个盒子
class _KeyDemoState extends State<KeyDemo> {

  List<Widget> items = [
    const StfulItem('1111'),
    const StfulItem('2222'),
    const StfulItem('3333'),
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("KeyDemo"),),
      body: Row(
        mainAxisAlignment: MainAxisAlignment.center,
        children: items,
      ),
      floatingActionButton: FloatingActionButton(
        child: const Icon(Icons.delete),
        onPressed: (){
          setState(() {
            items.removeAt(0);
          });
        },
      ),
    );
  }
}
复制代码

2.gif

把 color 生成不放到 state 里面

class StfulItem extends StatefulWidget {
  final String title;
  StfulItem(this.title, {Key? key}) : super(key: key);

  final color = Color.fromRGBO(
      Random().nextInt(256), Random().nextInt(256), Random().nextInt(256), 1.0);

  @override
  _StfulItemState createState() => _StfulItemState();
}
复制代码

1.gif

发现效果也是一个个删除,并没有发现颜色复用,因为这个是更新了 widget 导致的。

把上面 StatefulWidget 改成 StatelessWidget,看下效果会是什么样?

class StlItem extends StatelessWidget {

  final String title;

  StlItem(this.title, {Key? key}) : super(key: key);
  final color = Color.fromRGBO(
      Random().nextInt(256), Random().nextInt(256), Random().nextInt(256), 1.0);
  @override
  Widget build(BuildContext context) {
    return Container(
      width: 100,
      height: 100,
      child: Text(title),
      color: color,
    );
  }
}
复制代码

发现这个时候删除后是正常的,没有复用前一个的颜色。

3.gif

  • 针对第一种情况,会复用之前的颜色,假如使用 value Key 会是什么效果呢?
List<Widget> items = [
  StfulItem('1111', key: const ValueKey('1111')),
  StfulItem('2222', key: const ValueKey('2222')),
  StfulItem('3333', key: const ValueKey('3333')),
];
复制代码

发现确实达到了效果,没有在复用了,这里我就不贴运行的额效果了。

为什么 Stateful Widget 无法正常删除,加上了 Key 之后就可以了,在这之中到底发生了什么? 为了弄明白这个问题,我们会涉及 Widget 的 diff 更新机制。先介绍下 Flutter 的渲染原理

1.gif

Flutter 的渲染原理

并不是所有的Widget都会被独立渲染!只有继承RenderObjectWidget的才会创建RenderObject对象。在Flutter渲染的流程中,有三颗重要的树!Flutter引擎是针对Render树进行渲染!

Widget:Widget里面存储了一个视图的配置信息,包括布局、属性等。它是一份轻量的数据结构,在构建时是结构树,它不参与直接的绘制。

Element:Element是Widget的抽象,当一个Widget首次被创建的时候,那么这个Widget会通过Widget.createElement,创建一个element,挂载到Element Tree遍历视图树。在attachRootWidget函数中,把 widget交给了 RenderObjectToWidgetAdapter这座桥梁,Element创建的同时还持有 Widget和 RenderObject的引用。构建系统通过遍历Element Tree来创建RenderObject,每一个Element都具有一个唯一的key,当触发视图更新时,只会更新标记的需变化的Element。类似react中setState后虚拟dom树的更新。

RenderObject:在 RenderObject树中会发生 Layout、Paint的绘制事件,大部分绘图性能优化发生在这里,RenderObject Tree构建为Canvas的所需描述数据,加入到Layer Tree中,最终在Flutter Engine中进行视图合成并光栅化交给GPU。

Widget 更新机制

static bool canUpdate(Widget oldWidget, Widget newWidget) {
  return oldWidget.runtimeType == newWidget.runtimeType
      && oldWidget.key == newWidget.key;
}
复制代码

其实知道 Flutter 的渲染原理后,我们知道 Widget 只是一个配置且无法修改,而 Element 才是真正被使用的对象,并可以修改。当新的 Widget 到来时将会调用 canUpdate 方法,来确定这个 Element是否需要更新。canUpdate 对两个(新老) Widget 的 runtimeType 和 key 进行比较,从而判断出当前的 Element 是否需要更新。若 canUpdate 方法返回 true 说明不需要替换 Element,直接更新 Widget 就可以了。

Stateless 的比较

我们在使用 StatelessWidget 时没有传入 key ,并且 runtimeType 也是一致的,所以 canUpdate 返回 true。这个时候就需要 Widget 的 build 方法重新构建,因为 color 和 title 都在这个里面,所以就会看到正常的删除效果。

Stateful 的比较

我定义的 color 是在 state 里面的,Widget 并不保存 State,真正 hold State 的引用的是 Stateful Element。 我们在使用 StatefulWidget 时没有传人 key ,并且 runtimeType 也是一致的,所以 canUpdate 返回 true,更新 widget,StatefulWidget 的 Element 将复用上一个的。

当我们传入一个 value key 之后,canUpdate 返回为 false。Flutter 的将会认为这个 Element 需要被替换。然后重新生成一个新的 Element 对象装载到 Element 树上替换掉之前的 Element。

几种类型的 key

key 本身是一个抽象类,有一个工厂构造方法创建ValueKey

  • 直接子类主要有:LocalKey 和 GlobalKey

  • GlobalKey:帮助我们访问某个Widget的信息

  • LocalKey:它用来区别哪个Element要保留,哪个Element要删除。diff 算法核心所在

    • ValueKey : 以值作为参数 (数字,字符串,任意类型)
    • ObjectKey : 以对象作为参数
    • LocalKey:(创建唯一标识)

LocalKey 的三种类型

LocalKey 继承自 Key, 翻译过来就是局部键,LocalKey 在具有相同父级的 Element 中必须是惟一的。也就是说,LocalKey 在同一层级中必须要有唯一性。

ValueKey

bool operator ==(Object other) {
  if (other.runtimeType != runtimeType)
    return false;
  return other is ValueKey<T>
      && other.value == value;
}
复制代码

使用特定类型的值来标识自身的键,ValueKey 在最上面的例子中已经使用过了,他可以接收任何类型的一个对象来最为 key。

通过源码我们可以看到它重写了 == 运算符,在判断是否相等的时候首先判断了类型是否相等,然后再去判断 value 是否相等;

ObjectKey

bool operator ==(Object other) {
  if (other.runtimeType != runtimeType)
    return false;
  return other is ObjectKey
      && identical(other.value, value);
}
复制代码

ObjectKeyValueKey 最大的区别就是比较的算不一样,其中首先也是比较的类型,然后就调用 indentical 方法进行比较,其比较的就是内存地址,相当于 java 中直接使用 == 进行比较。而 LocalKey 则相当于 java 中的 equals 方法用来比较值的;

UniqueKey

bool operator ==(Object other) {
  if (other.runtimeType != runtimeType)
    return false;
  return other is ObjectKey
      && identical(other.value, value);
}
复制代码

每次重新 build 的时候,UniqueKey 都是独一无二的,所以就会导致无法找到对应的 Element,状态就会丢失。有一种做法就是把 UniqueKey 定义在 build 的外面,这样就不会出现状态丢失的问题了。

GlobalKey

GlobalKey 继承自 Key,相比与 LocalKey,他的作用域是全局的,而 LocalKey 只作用于当前层级。 整个应用程序里都是唯一的,所以同一个 GlobalKey 只能作用在一个widget上。可以通过 GlobalKey拿到所对应的 state 和 element 或者 widget ,用以改变 state 的状态或者变量值。

代码示例:

通过点击悬浮按钮修改内容的值

class GlobalKeyDemo extends StatelessWidget {

  GlobalKeyDemo({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('GlobalKeyDemo'),
      ),
      body: ChildPage(),
      floatingActionButton: FloatingActionButton(
        child: const Icon(Icons.add),
        onPressed: (){
         
        },
      ),
    );
  }
}

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

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

class _ChildPageState extends State<ChildPage> {
  int count = 0;
  String data = 'hello';
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Text(count.toString()),
          Text(data),
        ],
      ),
    );
  }
}
复制代码

我们会发现是不在同一个 widget 里面的,无法修改 ChildPage 中 text 文本的内容,这个时候就需要用到 GlobalKey 来实现

class GlobalKeyDemo extends StatelessWidget {
  
  //定义全局的key
  final GlobalKey<_ChildPageState> _globalKey = GlobalKey();

  GlobalKeyDemo({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('GlobalKeyDemo'),
      ),
      //把全局的key传到其他部件里面去
      body: ChildPage(key: _globalKey,),
      floatingActionButton: FloatingActionButton(
        child: const Icon(Icons.add),
        onPressed: (){
          _globalKey.currentState!.setState(() {
            _globalKey.currentState!.data = 'old:' + _globalKey.currentState!.count.toString();
            _globalKey.currentState!.count++;
          });
        },
      ),
    );
  }
}

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

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

class _ChildPageState extends State<ChildPage> {
  int count = 0;
  String data = 'hello';
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Text(count.toString()),
          Text(data),
        ],
      ),
    );
  }
}
复制代码

通过 Global Key 我们获取内容有以下API:

  • currentContext: 可以找到包括renderBox在内的各种element有关的东西
  • currentWidget: 可以得到widget的属性
  • currentState: 可以得到state里面的变量.

Global Key的实现原理

在 GlobalKey 内部有一个静态的_registry Map集合,该集合以 GlobalKey 为 key,以 Element 为Value 。其提供的 currentSate 方法就是以 GlobalKey 对象为 key 获取对应的 Element 对象,然后就可以通过 Element.state 获取具体的值。

我不知道为什么多文章介绍 GlobalKey是非常昂贵的,没有特别的复用需求,不建议使用它,通过打断点发现是会调用 unmount-> _unregisterGlobalKey进行销毁的,为什么还说昂贵呢!

实现效果:

Simulator Screen Shot - iPhone 12 Pro - 2022-01-06 at 14.10.37.png

小结

当你想要跨widget树保留状态时,应该使用key。当修改相同类型的widget集合时,,要将key放在要保留的widget的树的顶部。LocalKey 在同一层级中必须要有唯一性,GlobalKey 的作用域是全局的。

  • GlobalKey:帮助我们访问某个Widget的信息

  • LocalKey:它用来区别哪个Element要保留,哪个Element要删除。

    • ValueKey:以值作为参数 (数字,字符串,任意类型)
    • ObjectKey:以对象作为参数
    • LocalKey:(创建唯一标识)

参考文章:

Flutter的渲染原理:zhuanlan.zhihu.com/p/135969091

Flutter | 深入浅出Key:juejin.cn/post/684490…

猜你喜欢

转载自juejin.im/post/7050003302041255973