博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Flutter key
阅读量:4082 次
发布时间:2019-05-25

本文共 8335 字,大约阅读时间需要 27 分钟。

一,前言

在开发 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
{ List
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
{ final Color color = RandomColor().randomColor(); @override Widget build(BuildContext context) { return Container( width: 100, height: 100, color: color, ); }}

复制代码

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

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

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

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

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

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

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

  • Widget 更新机制

    下面来来看Widget的源码。

    复制代码

    @immutableabstract 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的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
    { List
    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
{ List
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

    复制代码

    @immutableabstract class Key {  const factory Key(String value) = ValueKey
    ; @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

    复制代码

    @optionalTypeArgsabstract class GlobalKey
    > extends Key {···static final Map
    _registry =
    {};static final Set
    _debugIllFatedElements = HashSet
    ();static final Map
    _debugReservations =
    {};···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
    { 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
    { final GlobalKey
    key = GlobalKey
    (); @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即可以在多个页面或者层级复用,比如两个页面也可也同时保持一个状态。

转载地址:http://uuhni.baihongyu.com/

你可能感兴趣的文章
归档与解归档
查看>>
为什么button在设置标题时要用一个方法,而不像lable一样直接用一个属性
查看>>
字符串的截取
查看>>
139. Word Break (DP)
查看>>
Tensorflow入门资料
查看>>
剑指_用两个栈实现队列
查看>>
剑指_栈的压入弹出序列
查看>>
剑指_复杂链表的复制
查看>>
FTP的命令
查看>>
CentOS操作系统下安装yum的方法
查看>>
FTP 常见问题
查看>>
zookeeper单机集群安装
查看>>
do_generic_file_read()函数
查看>>
Python学习笔记之数据类型
查看>>
Python学习笔记之特点
查看>>
shell 快捷键
查看>>
VIM滚屏操作
查看>>
EMC 2014存储布局及十大新技术要点
查看>>
linux内核内存管理(zone_dma zone_normal zone_highmem)
查看>>
将file文件内容转成字符串
查看>>