修炼Flutter | 深入浅出Key

前言

在开发 Flutter 的过程中你可能会发现,一些小部件的构造函数中都有一个可选的参数——Key。刚接触的同学或许会对这个概念感到很迷茫,感到不知所措。

在这篇文章中我们会深入浅出的介绍什么是 Key,以及应该使用 key 的具体场景。
在这里插入图片描述

什么是Key

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

我们来看看下面这个例子。

class StatelessContainer extends StatelessWidget {
  final Color color = RandomColor().randomColor();
  
  @override
  Widget build(BuildContext context) {
    return Container(
      width: 100,
      height: 100,
      color: color,
    );
  }
}

这是一个很简单的 Stateless Widget,显示在界面上的就是一个 100 * 100 的有颜色的 Container。
RandomColor 能够为这个 Widget 初始化一个随机颜色。

我们现在将这个Widget展示到界面上。

class Screen extends StatefulWidget {
  @override
  _ScreenState createState() => _ScreenState();
}

class _ScreenState extends State<Screen> {
  List<Widget> widgets = [
    StatelessContainer(),
    StatelessContainer(),
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: widgets,
        ),
      ),
      floatingActionButton: FloatingActionButton(
          onPressed: switchWidget,
        child: Icon(Icons.undo),
      ),
    );
  }

  switchWidget(){
    widgets.insert(0, widgets.removeAt(1));
    setState(() {});
  }
}

这里在屏幕中心展示了两个 StatelessContainer 小部件,当我们点击 floatingActionButton 时,将会执行 switchWidget 并交换它们的顺序。

                                                                    ![](https://img-blog.csdnimg.cn/img_convert/1101f645c4a2908b1f49da645756eb70.webp?x-oss-process=image/format,png)

看上去并没有什么问题,交换操作被正确执行了。现在我们做一点小小的改动,将这个 StatelessContainer 升级为 StatefulContainer。

class StatefulContainer extends StatefulWidget {
  StatefulContainer({Key key}) : super(key: key);
  @override
  _StatefulContainerState createState() => _StatefulContainerState();
}

class _StatefulContainerState extends State<StatefulContainer> {
  final Color color = RandomColor().randomColor();

  @override
  Widget build(BuildContext context) {
    return Container(
      width: 100,
      height: 100,
      color: color,
    );
  }
}

在 StatefulContainer 中,我们将定义 Color 和 build 方法都放进了 State 中。

现在我们还是使用刚才一样的布局,只不过把 StatelessContainer 替换成 StatefulContainer,看看会发生什么。

                                                     ![](https://img-blog.csdnimg.cn/img_convert/1ee039b0b8203291287d97aef9330fa0.webp?x-oss-process=image/format,png)

这时,无论我们怎样点击,都再也没有办法交换这两个Container的顺序了,而 switchWidget 确实是被执行了的。

为了解决这个问题,我们在两个 Widget 构造的时候给它传入一个 UniqueKey。

class _ScreenState extends State<Screen> {
  List<Widget> widgets = [
    StatefulContainer(key: UniqueKey(),),
    StatefulContainer(key: UniqueKey(),),
  ];
  ···

然后这两个 Widget 又可以正常被交换顺序了。

看到这里大家肯定心中会有疑问,为什么 Stateful Widget 无法正常交换顺序,加上了 Key 之后就可以了,在这之中到底发生了什么? 为了弄明白这个问题,我们将涉及 Widget 的 diff 更新机制。

Widget 更新机制

在之前的文章中,我们介绍了 WidgetElement 的关系。若你还对 Element 的概念感到很模糊的话,请先阅读 Flutter | 深入理解BuildContext

下面来来看Widget的源码。

@immutable
abstract class Widget extends DiagnosticableTree {
  const Widget({ this.key });
  final Key key;
  ···
  static bool canUpdate(Widget oldWidget, Widget newWidget) {
    return oldWidget.runtimeType == newWidget.runtimeType
        && oldWidget.key == newWidget.key;
  }
}

我们知道 Widget 只是一个配置且无法修改,而 Element 才是真正被使用的对象,并可以修改。

当新的 Widget 到来时将会调用 canUpdate 方法,来确定这个 Element是否需要更新。

canUpdate 对两个(新老) Widget 的 runtimeType 和 key 进行比较,从而判断出当前的 Element 是否需要更新。

StatelessContainer 比较过程

在 StatelessContainer 中,我们并没有传入 key ,所以只比较它们的 runtimeType。我们将 color 属性定义在了 Widget 中,这将导致他们具有不同的 runtimeType。所以在 StatelessContainer 这个例子中,Flutter能够正确的交换它们的位置。

StatefulContainer 比较过程

而在 StatefulContainer 的例子中,我们将 color 的定义放在了 State 中,Widget 并不保存 State,真正 hold State 的引用的是 Stateful Element。

当我们没有给 Widget 任何 key 的时候,将会只比较这两个 Widget 的 runtimeType 。由于两个 Widget 的属性和方法都相同,canUpdate 方法将会返回 false,在 Flutter 看来,并没有发生变化。所以这两个 Element 将不会交换位置。

而我们给 Widget 一个 key 之后,canUpdate 方法将会比较两个 Widget 的 runtimeType 以及 key。并返回 true,现在 Flutter 就可以正确的感知到两个 Widget 交换了顺序了。
(这里 runtimeType 相同,key 不同)

比较范围

为了提升性能 Flutter 的比较算法(diff)是有范围的,它并不是对第一个 StatefulWidget 进行比较,而是对某一个层级的 Widget 进行比较。

···
class _ScreenState extends State<Screen> {
  List<Widget> widgets = [
    Padding(
      padding: const EdgeInsets.all(8.0),
      child: StatefulContainer(key: UniqueKey(),),
    ),
    Padding(
      padding: const EdgeInsets.all(8.0),
      child: StatefulContainer(key: UniqueKey(),),
    ),
  ];
···

在这个例子中,我们将两个带 key 的 StatefulContainer 包裹上 Padding 组件,然后点击交换按钮,会发生下面这件奇妙的事情。

                                                           ![](https://img-blog.csdnimg.cn/img_convert/df5a1a1ba6e5f7e567eddff9262268a7.webp?x-oss-process=image/format,png)

两个 Widget 的 Element 并不是交换顺序,而是被重新创建了。

在 Flutter 的比较过程中它下到 Row 这个层级,发现它是一个 MultiChildRenderObjectWidget(多子部件的 Widget)。然后它会对所有 children 层逐个进行扫描。

首先它会查看第一个 padding,发现 padding 部分的 runtimeType 并没有改变。然后再比较第一个 padding 内部的 Widget,由于内部的 Widget 存在 key,并且现在的 key 应该是之前第二个 StatefulContainer 的 key,和原来的(第一个 StatefulContainer 的 key)对比发生了变化。且现在的层级在 padding 内部,该层级没有多子 Widget。Flutter 的比较算法将会认为这个 Element 被替换了。将会重新生成一个新的 Element 对象装载到 Element 树上。

然后 Flutter 继续在 Row 的 children 中继续往下查看下面的部件,第二个同理。

所以为了解决这个问题,我们需要将 key 放到 Row 的 children 这一层级。

···
class _ScreenState extends State<Screen> {
  List<Widget> widgets = [
    Padding(
      key: UniqueKey(),
      padding: const EdgeInsets.all(8.0),
      child: StatefulContainer(),
    ),
    Padding(
      key: UniqueKey(),
      padding: const EdgeInsets.all(8.0),
      child: StatefulContainer(),
    ),
  ];
···

现在我们又可以愉快的玩耍了(交换 Widget 顺序)了。

Key 的种类

Key

@immutable
abstract class Key {
  const factory Key(String value) = ValueKey<String>;

  @protected
  const Key.empty();
}

默认创建 Key 将会通过工厂方法根据传入的 value 创建一个 ValueKey。

Key 派生出两种不同用途的 Key:LocalKey 和 GlobalKey。

Localkey

LocalKey 直接继承至 Key,它应用于拥有相同父 Element 的小部件进行比较的情况,也就是上述例子中,有一个多子 Widget 中需要对它的子 widget 进行移动处理,这时候你应该使用Localkey。

Localkey 派生出了许多子类 key:

  • ValueKey : ValueKey(‘String’)
  • ObjectKey : ObjectKey(Object)
  • UniqueKey : UniqueKey()

Valuekey 又派生出了 PageStorageKey : PageStorageKey(‘value’)

GlobalKey

@optionalTypeArgs
abstract class GlobalKey<T extends State<StatefulWidget>> extends Key {
···
static final Map<GlobalKey, Element> _registry = <GlobalKey, Element>{};
static final Set<Element> _debugIllFatedElements = HashSet<Element>();
static final Map<GlobalKey, Element> _debugReservations = <GlobalKey, Element>{};
···
BuildContext get currentContext ···
Widget get currentWidget ···
T get currentState ···

GlobalKey 使用了一个静态常量 Map 来保存它对应的 Element。

你可以通过 GlobalKey 找到持有该GlobalKey的 WidgetStateElement

注意:GlobalKey 是非常昂贵的,需要谨慎使用。

什么时候需要使用 Key

ValueKey

如果您有一个 Todo List 应用程序,它将会记录你需要完成的事情。我们假设每个 Todo 事情都各不相同,而你想要对每个 Todo 进行滑动删除操作。

这时候就需要使用 ValueKey!

return TodoItem(
    key: ValueKey(todo.task),
    todo: todo,
    onDismissed: (direction){
        _removeTodo(context, todo);
    },
);

ObjectKey

如果你有一个生日应用,它可以记录某个人的生日,并用列表显示出来,同样的还是需要有一个滑动删除操作。

我们知道人名可能会重复,这时候你无法保证给 Key 的值每次都会不同。但是,当人名和生日组合起来的 Object 将具有唯一性。

这时候你需要使用 ObjectKey!

UniqueKey

如果组合的 Object 都无法满足唯一性的时候,你想要确保每一个 Key 都具有唯一性。那么,你可以使用 UniqueKey。它将会通过该对象生成一个具有唯一性的 hash 码。

不过这样做,每次 Widget 被构建时都会去重新生成一个新的 UniqueKey,失去了一致性。也就是说你的小部件还是会改变。(还不如不用?)

PageStorageKey

当你有一个滑动列表,你通过某一个 Item 跳转到了一个新的页面,当你返回之前的列表页面时,你发现滑动的距离回到了顶部。这时候,给 Sliver 一个 PageStorageKey!它将能够保持 Sliver 的滚动状态。

GlobalKey

GlobalKey 能够跨 Widget 访问状态。
在这里我们有一个 Switcher 小部件,它可以通过 changeState 改变它的状态。

class SwitcherScreenState extends State<SwitcherScreen> {
  bool isActive = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Switch.adaptive(
            value: isActive,
            onChanged: (bool currentStatus) {
              isActive = currentStatus;
              setState(() {});
            }),
      ),
    );
  }

  changeState() {
    isActive = !isActive;
    setState(() {});
  }
}

但是我们想要在外部改变该状态,这时候就需要使用 GlobalKey。

class _ScreenState extends State<Screen> {
  final GlobalKey<SwitcherScreenState> key = GlobalKey<SwitcherScreenState>();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SwitcherScreen(
        key: key,
      ),
      floatingActionButton: FloatingActionButton(onPressed: () {
        key.currentState.changeState();
      }),
    );
  }
}

这里我们通过定义了一个 GlobalKey 并传递给 SwitcherScreen。然后我们便可以通过这个 key 拿到它所绑定的 SwitcherState 并在外部调用 changeState 改变状态了。

                                                                           ![](https://img-blog.csdnimg.cn/img_convert/724f9c4efdd3564cde9183332263dd7e.webp?x-oss-process=image/format,png)

参考资料

写在最后

这篇文章的灵感来自于 何时使用密钥 - Flutter小部件 101 第四集, 强烈建议大家观看这个系列视频,你会对 Flutter 如何构建视图更加清晰。也希望这篇文章对你有所帮助!

在这里为了方便大家系统的学习Flutter,这里特意联合了阿里P7架构师和谷歌技术团队共同整理了一份Flutter全家桶学习资料。

内容概要:Flutter技术解析与实战、Flutter进阶学习笔记、Flutter入门与实战和Flutter完整开发实战详解。

内容特点:条理清晰,含图像化表示更加易懂。

由于文章内容比较多,篇幅有限,资料已经被整理成了PDF文档,有需要 Flutter技术解析与实战 完整文档的可扫描下方卡片免费获取!

《Flutter技术解析与实战》

目录

第一章 混合工程

​ ● Flutter工程体系

​ ● 混合工程改造实战

​ ● 混合工程与持续集成

​ ● 快速完成混合工程搭建

​ ● 使用混合栈框架开发

img

第二章 能力增强

​ ● 基于原生能力的插件扩展

​ ● 基于外接纹理的同层渲染

​ ● 多媒体能力扩展实践

​ ● 富文本能力应用实践

第三章 业务架构设计

​ ● 应用框架设计实践

​ ● 轻量级动态化渲染引擎的设计

​ ● 面向切面编程的设计实践

​ ● 高性能的动态模板渲染实践

img

第四章 数据统计与性能

​ ● 数据统计框架的设计

​ ● 性能稳定性监控方案的设计

​ ● 高可用框架的设计与实践

​ ● 跨端方案性能对比实践

img

第五章 企业级应用实战

​ ● 基于Flutter的端结构演进与创新

​ ● Flutter与FaaS云端一体化架构

img

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值