一,前言

在开发 Flutter 的过程中你可能会发现,一些小部件的构造函数中都有一个可选的参数——Key。在这篇文章中我们会深入浅出的介绍什么是 Key,以及应该使用 key 的具体场景。

二,什么是Key

在 Flutter 中我们经常与状态打交道。我们知道 Widget 可以有 StatefulStateless 两种。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 并交换它们的顺序。

看上去并没有什么问题,交换操作被正确执行了。现在我们做一点小小的改动,将这个 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 中,我们将定义 Colorbuild方法都放进了 State 中。

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

这时,无论我们怎样点击,都再也没有办法交换这两个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 更新机制

    下面来来看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 的 runtimeTypekey 进行比较,从而判断出当前的 Element 是否需要更新

    • StatelessContainer 比较过程

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

      而在 StatefulContainer 的例子中,我们将 color 的定义放在了 State 中,Widget 并不保存 State,真正 hold State 的引用的是 Stateful Element。当我们没有给 Widget 任何 key 的时候,将会只比较这两个 WidgetruntimeType 。由于两个 Widget 的属性和方法都相同,canUpdate 方法将会返回 false,在 Flutter 看来,并没有发生变化。所以这两个 Element 将不会交换位置。而我们给 Widget 一个 key 之后,canUpdate 方法将会比较两个 Widget 的 runtimeType 以及 key。并返回 true,现在 Flutter 就可以正确的感知到两个 Widget 交换了顺序了。 (这里 runtimeType 相同,key 不同)
    • 总结:

      我们在构建Flutter的UI时是以Widget的形式『拼接』出来的,组件树作为UI每一个组件都对应一个元素(原文中是Slot),从而形成了『元素树』(Element Tree),元素树的内容非常简单,只包含了组件的类型和子元素的引用(Type),你可以把元素树当做Flutter App中的骨架(skeleton),它只展现了App的结构,并不包含其他具体的信息。
      
      当我们交换组件树中的元素时,组件确实进行了交换,但是元素树却不一定。Flutter会先遍历(walk)整个元素树,从Row上的主元素,到主元素的子元素,查看整体的结构是否发生了变化,当然,它检查的只能是元素的Type和Key,在给出的例子中,当我们不设置Key时,元素树对比Type,发现Type并没有发生变化,而Flutter却是用元素树和元素对应的状态(可用或者不可用),来决定这个元素是否应该显示出来,所以在界面中并没有发生改变,但是当我们加入Key之后,对比的对象多了一个,并且是和之前不一样的,Flutter察觉到之后,立即改变了元素的状态,让它变为『无用状态』(deactivate),当遍历完之后,Flutter会浏览(look through)这些不匹配的元素(non-matched children)通过相应的引用为之找到对应的组件。当所有的元素都匹配完成之后,Flutter会刷新界面,展现出我们预想的。

              

  • 比较范围

     为了提升性能 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 组件,然后点击交换按钮,会发生下面这件奇妙的事情。

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

分析:(1)我们分析一下这次的Widget Tree 和 Element Tree,当我们交换元素后,Flutter element-to-widget matching algorithm,(元素-组件匹配算法),开始进行对比,算法每次只对比一层,即Padding这一层。显然,Padding并没有发生本质的变化。

  

       (2)于是开始进行第二层对比,在对比时Flutter发现元素与组件的Key并不匹配,于是,把它设置成不可用状态,但是这里所使用的Key只是本地Key(Local Key),Flutter并不能找到另一层里面的Key(即另外一个Padding Widget中的Key)所以,Flutter就创建了一个新的Widget,而这个Widget的颜色就成了我们看到的『随机色』。  

  总结:
  

  所以为了解决这个问题,我们需要将 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:LocalKeyGlobalKey

  

  • 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的 Widget,State 和 Element

    注意: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<SwitcherScreenState> 并传递给 SwitcherScreen。然后我们便可以通过这个 key 拿到它所绑定的 SwitcherState 并在外部调用 changeState 改变状态了。

五,总结:

上面的例子,因为没有数据,所以使用了UniqueKey,在真实的开发中,我们可以用Model中的id作为ObjectKey。
GlobalKey其实是对应于LocalKey,上面我们说Padding中的就是LocalKey,Global即可以在多个页面或者层级复用,比如两个页面也可也同时保持一个状态。

【Flutter学习】之深入浅出 Key的更多相关文章

  1. 命令——WPF学习之深入浅出

    WPF学习之深入浅出话命令   WPF为我们准备了完善的命令系统,你可能会问:“有了路由事件为什么还需要命令系统呢?”.事件的作用是发布.传播一些消息,消息传达到了接收者,事件的指令也就算完成了,至于 ...

  2. Flutter学习笔记(3)--Dart变量与基本数据类型

    一.变量 在Dart里面,变量的声明使用var.Object或Dynamic关键字,如下所示: var name = ‘张三’: 在Dart语言里一切皆为对象,所以如果没有将变量初始化,那么它的默认值 ...

  3. Flutter学习笔记(9)--组件Widget

    如需转载,请注明出处:Flutter学习笔记(9)--组件Widget 在Flutter中,所有的显示都是Widget,Widget是一切的基础,我们可以通过修改数据,再用setState设置数据(调 ...

  4. Flutter学习笔记(10)--容器组件、图片组件

    如需转载,请注明出处:Flutter学习笔记(10)--容器组件.图片组件 上一篇Flutter学习笔记(9)--组件Widget我们说到了在Flutter中一个非常重要的理念"一切皆为组件 ...

  5. Flutter学习笔记(13)--表单组件

    如需转载,请注明出处:Flutter学习笔记(13)--表单组件 表单组件是个包含表单元素的区域,表单元素允许用户输入内容,比如:文本区域,下拉表单,单选框.复选框等,常见的应用场景有:登陆.注册.输 ...

  6. Flutter学习笔记(17)--顶部导航TabBar、TabBarView、DefaultTabController

    如需转载,请注明出处:Flutter学习笔记(17)--顶部导航TabBar.TabBarView.DefaultTabController 上一篇我们说了BottmNavigationBar底部导航 ...

  7. Flutter学习笔记(20)--FloatingActionButton、PopupMenuButton、SimpleDialog、AlertDialog、SnackBar

    如需转载,请注明出处:Flutter学习笔记(20)--FloatingActionButton.PopupMenuButton.SimpleDialog.AlertDialog.SnackBar F ...

  8. Flutter学习笔记(21)--TextField文本框组件和Card卡片组件

    如需转载,请注明出处:Flutter学习笔记(21)--TextField文本框组件和Card卡片组件 今天来学习下TextField文本框组件和Card卡片组件. 只要是应用程序就少不了交互,基本上 ...

  9. Flutter学习笔记(24)--SingleChildScrollView滚动组件

    如需转载,请注明出处:Flutter学习笔记(23)--多 在我们实际的项目开发中,经常会遇到页面UI内容过多,导致手机一屏展示不完的情况出现,以Android为例,在Android中遇到这类情况的做 ...

  10. Flutter学习笔记(29)--Flutter如何与native进行通信

    如需转载,请注明出处:Flutter学习笔记(29)--Flutter如何与native进行通信 前言:在我们开发Flutter项目的时候,难免会遇到需要调用native api或者是其他的情况,这时 ...

随机推荐

  1. SSH弱小算法支持问题

    SSH弱小算法支持问题:SSH的配置文件中加密算法没有指定(没有配置加密算法),则会默认支持所有加密算法,包括arcfour,arcfour128,arcfour256等弱加密算法.解决方法:1.修改 ...

  2. js获取URL地址的参数

    function getQueryString(name) { var reg = new RegExp("(^|&)" + name + "=([^&] ...

  3. [CSP-S模拟测试]:Walk(BFS+建边)

    题目描述 在比特镇一共有$n$个街区,编号依次为$1$到$n$,它们之间通过若干条单向道路连接. 比特镇的交通系统极具特色,除了$m$条单向道路之外,每个街区还有一个编码${val}_i$,不同街区可 ...

  4. mysql启动以及常用命令汇总

    mysql57的启动 常用命令 : show databases        :            展示所有数据库 use  数据库名      :     连接数据库 show tables ...

  5. Dealing with exceptions thrown in Application_Start()

    https://blog.richardszalay.com/2007/03/08/dealing-with-exceptions-thrown-in-application_start/ One a ...

  6. codeforces 582A GCD Table

    题意简述: 给定一个长度为$n$的序列 将这个序列里的数两两求$gcd$得到$n^2$个数 将这$n^2$个数打乱顺序给出 求原序列的一种可能的情况 ------------------------- ...

  7. SercletConfig 详解

    ServletConfig:从一个servlet被实例化后,对任何客户端在任何时候访问有效,但仅对本servlet有效,一个servlet的ServletConfig对象不能被另一个servlet访问 ...

  8. 安装SSH2拓展 PHP上传文件到远程服务器

    情景:客户端上传图片到服务器A,服务器A同步上传至另外一个静态资源服务器B 环境:php7 linux(ubuntu) 安装php的ssh2扩展 -dev sudo apt-get install p ...

  9. Maven 上传文件 Error creating bean with name 'multipartResolver':

    <!--配置MultipartResolver 处理文件上传--><bean id="multipartResolver" class="org.spr ...

  10. Java集合的介绍

    参考博客: https://blog.csdn.net/zhangqunshuai/article/details/80660974 List , Set, Map都是接口,前两个继承至Collect ...