说说Flutter中最熟悉的陌生人 —— Key

Posted 唯鹿

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了说说Flutter中最熟悉的陌生人 —— Key相关的知识,希望对你有一定的参考价值。

Key在Flutter的源码中可以说是无处不在,但是我们日常中确不怎么使用它。有点像是“最熟悉的陌生人”,那么今天就来说说这个“陌生人”,揭开它神秘的面纱。

概念

KeyWidgetElementSemanticsNode的标识符。 只有当新的WidgetKey与当前ElementWidgetKey相同时,它才会被用来更新现有的ElementKey在具有相同父级的Element之间必须是唯一的。

以上定义是源码中关于Key的解释。通俗的说就是Widget的标识,帮助实现Element的复用。关于它的说明源码中也提供了YouTube的视频链接:When to Use Keys。如果你无法访问,可以看Google 官方在优酷上传的

When to Use Keys

例子

视频中的例子很简单且具有代表性,所以本文将采用它来介绍今天的内容。

首先上代码:

import 'dart:math';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget 
  @override
  Widget build(BuildContext context) 
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Home Page'),
    );
  


class MyHomePage extends StatefulWidget 
  MyHomePage(Key key, this.title) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();


class _MyHomePageState extends State<MyHomePage> 
  List<Widget> widgets;

  @override
  void initState() 
    super.initState();
    widgets = [
      StatelessColorfulTile(),
      StatelessColorfulTile()
    ];
  

  @override
  Widget build(BuildContext context) 
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Row(
        children: widgets,
      ),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.refresh),
        onPressed: _swapTile,
      ),
    );
  

  _swapTile() 
    setState(() 
      widgets.insert(1, widgets.removeAt(0));
    );
  


class StatelessColorfulTile extends StatelessWidget 

  final Color _color = Utils.randomColor();

  @override
  Widget build(BuildContext context) 
    return Container(
      height: 150,
      width: 150,
      color: _color,
    );
  


class Utils 
  static Color randomColor() 
    var red = Random.secure().nextInt(255);
    var greed = Random.secure().nextInt(255);
    var blue = Random.secure().nextInt(255);
    return Color.fromARGB(255, red, greed, blue);
  

代码可以直接复制到DartPad中运行查看效果。 或者点击这里直接运行

效果很简单,就是两个彩色方块,点击右下角的按钮后交换两个方块的位置。这里我就不放具体的效果图了。实际效果也和我们预期的一样,两个方块成功交换位置。

发现问题

上面的方块是StatelessWidget,那我们把它换成StatefulWidget呢?。

class StatefulColorfulTile extends StatefulWidget 
  StatefulColorfulTile(Key key) : super(key: key);

  @override
  StatefulColorfulTileState createState() => StatefulColorfulTileState();


class StatefulColorfulTileState extends State<StatefulColorfulTile> 
  final Color _color = Utils.randomColor();

  @override
  Widget build(BuildContext context) 
    return Container(
      height: 150,
      width: 150,
      color: _color,
    );
  

再次执行代码,发现方块没有“交换”。这是为什么?

分析问题

首先要知道Flutter中有三棵树,分别是Widget TreeElement TreeRenderObject Tree

  • Widget: Element配置信息。与Element的关系可以是一对多,一份配置可以创造多个Element实例。
  • Element:Widget 的实例化,内部持有WidgetRenderObject
  • RenderObject:负责渲染绘制

简单的比拟一下,Widget有点像是产品经理,规划产品整理需求。Element则是UI小姐姐,根据原型整理出最终设计图。RenderObject就是我们程序员,负责具体的落地实现。

代码中可以确定一点,两个方块的Widget肯定是交换了。既然Widget没有问题,那就看看Element

但是为什么StatelessWidget可以成功,换成StatefulWidget就失效了?

点击按钮调用setState方法,依次执行:

标记自身元素dirty为true 添加至_dirtyElements _element.markNeedsBuild() owner.scheduleBuildFor() drawFrame() buildScope() _dirtyElements[index].rebuild() performRebuild() updateChild()

我们重点看一下ElementupdateChild方法:

  @protected
  Element updateChild(Element child, Widget newWidget, dynamic newSlot) 
  	// 如果'newWidget'为null,而'child'不为null,那么我们删除'child',返回null。
    if (newWidget == null) 
      if (child != null)
        deactivateChild(child);
      return null;
    
    if (child != null) 
      // 两个widget相同,位置不同更新位置,返回child。这里比较的是hashCode
      if (child.widget == newWidget) 
        if (child.slot != newSlot)
          updateSlotForChild(child, newSlot);
        return child;
      
      // 我们的交换例子处理在这里
      if (Widget.canUpdate(child.widget, newWidget)) 
        if (child.slot != newSlot)
          updateSlotForChild(child, newSlot);
        child.update(newWidget);
        return child;
      
      deactivateChild(child);
    
    // 如果无法更新复用,那么创建一个新的Element并返回。
    return inflateWidget(newWidget, newSlot);
  

WidgetcanUpdate方法:

  static bool canUpdate(Widget oldWidget, Widget newWidget) 
    return oldWidget.runtimeType == newWidget.runtimeType
        && oldWidget.key == newWidget.key;
  

这里出现了我们今天的主角Key,不过我们先放在一边。canUpdate方法的作用是判断newWidget是否可以替代oldWidget作为Element的配置。 一开始也提到了,Element会持有Widget。

该方法判断的依据就是runtimeTypekey是否相等。在我们上面的例子中,不管是StatelessWidget还是StatefulWidget的方块,显然canUpdate都会返回true。因此执行child.update(newWidget)方法,就是将持有的Widget更新了。

不知道这里大家有没有注意到,这里并没有更新state。我们看一下StatefulWidget源码:

abstract class StatefulWidget extends Widget 

  const StatefulWidget( Key key ) : super(key: key);
  
  @override
  StatefulElement createElement() => StatefulElement(this);

  @protected
  State createState();

StatefulWidget中创建的是StatefulElement,它是Element的子类。

class StatefulElement extends ComponentElement 

  StatefulElement(StatefulWidget widget)
      : _state = widget.createState(),
        super(widget) 
    _state._element = this;  
    _state._widget = widget;
  

  @override
  Widget build() => state.build(this);

  State<StatefulWidget> get state => _state;
  State<StatefulWidget> _state;
  ...

通过调用StatefulWidgetcreateElement方法,最终执行createState创建出state并持有。也就是说StatefulElement才持有state。

所以我们上面两个StatefulWidget的方块的交换,实际只是交换了“身体”,而“灵魂”没有交换。所以不管你怎么点击按钮都是没有变化的。

解决问题

找到了原因,那么怎么解决它?那就是设置一个不同的Key

  @override
  void initState() 
    super.initState();
    widgets = [
      StatefulColorfulTile(key: const Key("1")),
      StatefulColorfulTile(key: const Key("2"))
    ];
  

但是这里要注意的是,这里不是说添加key以后,在canUpdate方法返回false,最后执行inflateWidget(newWidget, newSlot)方法创建新的Element。(很多相关文章对于此处的说明都有误区。。。好吧我承认我一开始也被误导了。。。)

  @protected
  Element inflateWidget(Widget newWidget, dynamic newSlot) 
    final Key key = newWidget.key;
    if (key is GlobalKey) 
      final Element newChild = _retakeInactiveElement(key, newWidget);
      if (newChild != null) 
        newChild._activateWithParent(this, newSlot);
        final Element updatedChild = updateChild(newChild, newWidget, newSlot);
        assert(newChild == updatedChild);
        return updatedChild;
      
    
    // 这里就调用到了createElement,重新创建了Element
    final Element newChild = newWidget.createElement();
    newChild.mount(this, newSlot);
    return newChild;
  

如果如此,那么执行createElement方法势必会重新创建state,那么方块的颜色也就随机变了。当然此种情况并不是不存在,比如我们给现有的方块外包一层PaddingSingleChildRenderObjectElement):

  @override
  void initState() 
    super.initState();
    widgets = [
      Padding(
        padding: const EdgeInsets.all(8.0),
        child: StatefulColorfulTile(key: Key("1"),)
      ),
      Padding(
        padding: const EdgeInsets.all(8.0),
        child: StatefulColorfulTile(key: Key("2"),)
      ),
    ];
  

这种情况下,交换后比较外层Padding不变,接着比较内层StatefulColorfulTile,因为key不相同导致颜色随机改变。因为两个方块位于不同子树,两者在逐层对比中用到的就是canUpdate方法返回false来更改。

而本例是方块的外层是RowMultiChildRenderObjectElement),是对比两个List,存在不同。关键在于update时调用的RenderObjectElement.updateChildren方法。

  @protected
  List<Element> updateChildren(List<Element> oldChildren, List<Widget> newWidgets,  Set<Element> forgottenChildren ) 
  	...
    int newChildrenTop = 0;
    int oldChildrenTop = 0;
    int newChildrenBottom = newWidgets.length - 1;
    int oldChildrenBottom = oldChildren.length - 1;

    final List<Element> newChildren = oldChildren.length == newWidgets.length ?
        oldChildren : List<Element>(newWidgets.length);

    Element previousChild;

    // 从前往后依次对比,相同的更新Element,记录位置,直到不相等时跳出循环。
    while ((oldChildrenTop <= oldChildrenBottom) && 
    	(newChildrenTop <= newChildrenBottom)) 
      final Element oldChild = replaceWithNullIfForgotten(oldChildren[oldChildrenTop]);
      final Widget newWidget = newWidgets[newChildrenTop];
      // 注意这里的canUpdate,本例中在没有添加key时返回true。
      // 因此直接执行updateChild,本循环结束返回newChildren。后面因条件不满足都在不执行。
      // 一旦添加key,这里返回false,不同之处就此开始。
      if (oldChild == null || !Widget.canUpdate(oldChild.widget, newWidget))
        break;
      final Element newChild = updateChild(oldChild, newWidget, previousChild);
      newChildren[newChildrenTop] = newChild;
      previousChild = newChild;
      newChildrenTop += 1;
      oldChildrenTop += 1;
    

    // 从后往前依次对比,记录位置,直到不相等时跳出循环。
    while ((oldChildrenTop <= oldChildrenBottom) && 
    	(newChildrenTop <= newChildrenBottom)) 
      final Element oldChild = replaceWithNullIfForgotten(oldChildren[oldChildrenBottom]);
      final Widget newWidget = newWidgets[newChildrenBottom];
      if (oldChild == null || !Widget.canUpdate(oldChild.widget, newWidget))
        break;
      oldChildrenBottom -= 1;
      newChildrenBottom -= 1;
    
	// 至此,就可以得到新旧List中不同Weiget的范围。
    final bool haveOldChildren = oldChildrenTop <= oldChildrenBottom;
    Map<Key, Element> oldKeyedChildren;
    // 如果存在中间范围,扫描旧children,获取所有的key与Element保存至oldKeyedChildren。
    if (haveOldChildren) 
      oldKeyedChildren = <Key, Element>;
      while (oldChildrenTop <= oldChildrenBottom) 
        final Element oldChild = replaceWithNullIfForgotten(oldChildren[oldChildrenTop]);
        if (oldChild != null) 
          if (oldChild.widget.key != null)
            oldKeyedChildren[oldChild.widget.key] = oldChild;
          else
          	// 没有key就移除对应的Element
            deactivateChild(oldChild);
        
        oldChildrenTop += 1;
      
    
	// 更新中间不同的部分
    while (newChildrenTop <= newChildrenBottom) 
      Element oldChild;
      final Widget newWidget = newWidgets[newChildrenTop];
      if (haveOldChildren) 
        final Key key = newWidget.key;
        if (key != null) 
          // key不为null,通过key获取对应的旧Element
          oldChild = oldKeyedChildren[key];
          if (oldChild != null) 
            if (Widget.canUpdate(oldChild.widget, newWidget)) 
              oldKeyedChildren.remove(key);
             else 
              oldChild = null;
            
          
        
      
      // 本例中这里的oldChild.widget与newWidget hashCode相同,在updateChild中成功被复用。
      final Element newChild = updateChild(oldChild, newWidget, previousChild);
      newChildren[newChildrenTop] = newChild;
      previousChild = newChild;
      newChildrenTop += 1;
    
    
    // 重置
    newChildrenBottom = newWidgets.length - 1;
    oldChildrenBottom = oldChildren.length - 1;

    // 将后面相同的Element更新后添加到newChildren,至此形成新的完整的children。
    while ((oldChildrenTop <= oldChildrenBottom) && 
    	(newChildrenTop <= newChildrenBottom)) 
      final Element oldChild = oldChildren[oldChildrenTop];
      final Widget newWidget = newWidgets[newChildrenTop]说说Flutter中的Semantics

熟悉而陌生——那些个系统抽象

C#基础构造函数:最熟悉的陌生人

说说非对称加密

See you again! Marvin

面试官:说说对单例模式的理解,最后的枚举实现我居然不知