flutter key 详解

简介: flutter key 详解

每个示例都是以 main_0x.dart 格式命名的,想看哪个示例,把它重命名为 main.dart ,运行即可。

Flutter 3.3.6 环境所有示例测试通过。

所有示例代码地址 gitee.com/iam17/flutt…

key 是用来做什么的

key 是 Widget ,Element 和 SemanticsNode 的身份标识。

key 总的来说分两种,一种是 Locakey ,一种是 GlobalKey。区别就在于 Locakey 的作用范围在兄弟节点之间,GlobalKey 的作用范围是全局。Locakey 在兄弟节点中唯一,GlobalKey在整个 App 唯一。

Localkey 的作用

我没有用key,也没什么问题啊?一般情况下,不用 指定 Key参数是没有什么问题的。但是如果要交换两个 widget 的位置呢?或是有 widget 被删除呢?这个时候如果没有 key,就无法确定这个 widget 去更新哪个 element 了。在key 为 null的情况下,如果类型相同,会更新同一位置的 element。

Localkey 的作用是用来决定同一 parent 下是否可以重用 element 的。

举个例子,有两个 Box Widget,点击 floatingActionButton 后,文本会交换,但是颜色没有交换。

image.png


完整代码点这里 gitee.com/iam17/flutt…

为什么会有这样的效果呢?这得从头说起。

为了提高效率,flutter 有三棵树,分别由 Widget,Element,RenderObject 组成。

  • Widget:Widget 相当于是配置文件,是不可变的。想改变,只能用一个新 widget 替换原来的 widget
  • Element:Element 是实例化的 Widget 对象,通过 Widget 的 createElement() 方法,根据 Widget数据生成。如果 widget 有相同的 runtimeType 并且有两同的 key, Element 可以根据新的 widget 进行 update.
  • RenderObject:用于应用界面的布局和绘制,保存了元素的大小,布局等信息.

两个关键逻辑

一、 判断 newWidget 是否可以更新持有 oldWidget 的 element。

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

在本例中,box1,box2 的 runtimeType 都是 Box,key 都为 null,所以 box1,box2是可以互相更新的。

二、 element 更新逻辑

element 的更新逻辑在 Element 类的 updateChild 方法中。更新逻辑如下

用 newWidget 更新 child。

  1. chid==null && newWidget!=null , 用 newWidget create new child。
  2. child.widget == newWidget,如果位置不同,更新child的位置。
  3. newWidget==null && child!=null ,删除 child
  4. newWidget!=null && child!=null,根据逻辑一中的判断逻辑判断 child是否可以更新,如果可以更新,用 newWidget 更新 child,否则 dispose 原 child,重建新的 child。
  5. child==null && newWidget==null 什么都不做

本例中,会命中第 4 条逻辑,更新文本,颜色不变,因为 element 的位置没变。

如果想让 element 更新位置,那就需要加上 LocalKey 了

在代码文件 main_01.dart 中找到这句

var children = [const Box(text: 'one'), const Box(text: 'two')];
复制代码

加上 LocalKey

var children = [
    const Box(text: 'one',key: ValueKey('one'),
    ),
    const Box(text: 'two',key: ValueKey('two'),),
  ];
复制代码

加上 key 之后,当 box widget 交换的时候,命中第二条逻辑,box widget 在 parent 中的位置发生了改变 ,更新 child,也就是 对应 element 的位置。

你可能会疑惑,为什么没有命中第四条?因为 box1,box2 的父级,也就是 column 在调用 updateChild(Element? child, Widget? newWidget, Object? newSlot)方法的时候,已经根据 widget 的 key 找到了对应的 child,调整了 child 的出场顺序,所以会命中第 2 条逻辑。

column 执行 updateChilren 方法调用 updateChild 准备 oldChild 的逻辑如下

//准备要更新的 child
Element? oldChild;
//用来更新 oldChild 的 widget
final Widget newWidget = newWidgets[newChildrenTop];
if (haveOldChildren) {
  final Key? key = newWidget.key;
  if (key != null) {
    //通过key 找到 newWidget 对应的child
    oldChild = oldKeyedChildren![key];
    if (oldChild != null) {
      if (Widget.canUpdate(oldChild.widget, newWidget)) {
        // we found a match!
        // remove it from oldKeyedChildren so we don't unsync it later
        oldKeyedChildren.remove(key);
      } else {
        // Not a match, let's pretend we didn't see it for now.
        oldChild = null;
      }
    }
  }
}
final Element newChild = 
   updateChild(oldChild, newWidget, slotFor(newChildrenTop, previousChild))!;
复制代码

通过源码可以知道 key 的重要性,如果没有 key 是无法根据 widget 找到对应的 child 的。

如果没有 key ,可能无法更新对应的 element。如果有了 key,比如本例中 column 就可以把 widget 和 child 配对,更新child的位置。

思考一下,如果交换 Box 的同时,同时修改 text,会走element 更新逻辑的第几条?

答案在本文最后。

Local key 是抽象类,无法直接用,可以用它的子类,ValueKey,ObjectKey,UniqueKey,PageStorageKey。

ValueKey

构造函数

const ValueKey(this.value);  
final T value;
复制代码

可以这样生成 key ValueKey<String>('1'),但一般我们都会省略范型,直接写 ValueKey('1'),dart 会自己推断出类型。

ValueKey(1),ValueKey('1'),是两个不同的 valueKey,因为它们的类型不同。ValueKey<int>(1),ValueKey<double>(1),是两个不同的 valueKey,虽然它们的值相同,但类型不同。

因为有值相同但类型不同的这种情况存在,所以在判断valueKey是否相等的时候,同时判断 类型和值

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

ValueKey 是最常用的 key,一般来说对于一个列表,如果有 id,用每个条目 的 id 作为 key 最合适不过。不要用索引作为 key,因为如果有删除或修改索引会发生改变,不能作为标识条目的作用。

ObjectKey

构造函数

const ObjectKey(this.value);
 final Object? value;
复制代码

看起来像是 ValueKey的具像化。T 具体为 Object。其实不然,ObjectKey 判断相等 和 ValueKey 不同。ValueKey 的相等条件是 类型相同且值相同,ObjectKey的相等条件是 类型相同且引用相同

判断引用举例:

var o = new Object();

  • ObjectKey(o),ObjectKey(o) 相同,o 是同一个Object
  • ObjectKey(const Object()),ObjectKey(const Object()) 相同
  • ObjectKey([1]),ObjectKey([1]) 不同。
  • ObjectKey(const [1]),ObjectKey(const [1]) 相同。
  • ObjectKey(const [1]),ObjectKey(const [2]) 不同。
  • ObjectKey(2,1+1),ObjectKey(1),相同。

我们尽量加上 const 关键字,有相同的 key,有利于优化。

StatelessWidget 子类和 StatefulWidget 子类加上 const , ObjectKey 也不同。

class Test extends StatelessWidget {
  const Test({super.key});
  @override
  Widget build(BuildContext context) {
    return Container();
  }
}
print(ObjectKey(const Test()) == ObjectKey(const Test())); //false
复制代码

UniqueKey

class UniqueKey extends LocalKey {
  UniqueKey();
  @override
  String toString() => '[#${shortHash(this)}]';
}
复制代码

没错,整个 UniqueKey 的代码就这么多,再往上看 LocalKey,key也什么都没有。每个 UniqueKey 的实例对象都是不相等的,它的应用场景是什么呢?

场景一 必须用不同key的场景 AnimatedSwitcher 的 child 不同才能进行动画 ,所以用 UniqueKey 比较合适。


image.png


完整代码  gitee.com/iam17/flutt…

只有 Widget 的 canUpdate 判定为假的时候,才会执行 Switcher 动画,因为类型没有变,所以只能改变 key,这里用 UniqueKey 保证每个 key 都不同,所以每次都会执行动画。

Text(
  '$count',
  key: UniqueKey(),
  style: const TextStyle(
      fontSize: 100, fontWeight: FontWeight.bold, color: Colors.blue),
 ),
复制代码

场景二 为了省事,不需要为 value 值而烦恼。先把key 生成好,每次都复用这个 key

class _MyWidgetState extends State<MyWidget> {
  var key1 = UniqueKey();
  var key2 = UniqueKey();
  @override
  Widget build(BuildContext context) {
   return  Row(children: const [Text('text1',key:key1),Text('text2',key:key2)],) 
  }
}
复制代码

key生成好了后,在 state 没 dispose之前是不变的。 每次调用  build 方法都会复用 这个 key。

PageStorageKey

PageStorageKey 是 ValueKey的子类,它的用处是保持滚动位置。

有两个tab ,第一个 tabview 有 PageStorageKey,第二个tabview 没有 key。结果就是,第一个tab 保持了 滚动位置 ,第二个 tab 失去了滚动位置。


image.png


完整代码 gitee.com/iam17/flutt…

关键代码就是加 PageStorageKey 的这句

ListView.builder(
                key:const PageStorageKey(1)
复制代码

要想操持滚动位置 必须用 PageStorageKey ,用其它类型的 key 是不行的。

使用 GlobalKey

在整个 app 范围内 Globalkey 是唯一的。GlobalKey不应该在build方法中初始化,否则会每次build都重建 GlobalKey。为了不让GlobalKeyt每次都重新生成,可以让State对象拥有GlobalKey对象,然后在 State.initState 的方法中初始化 GlobalKey。

应用场景有两个

一、获取相关的 BuildContext 或 State

示例很简单,就是一个按钮,点击后换颜色,完整代码  gitee.com/iam17/flutt…

主要代码如下:

var key = GlobalKey<_BoxState>();
MaterialApp(
        home: Scaffold(
      body: Box(key: key),
      floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
      floatingActionButton: ElevatedButton(
          onPressed: () {
            RenderBox box = key.currentContext?.findRenderObject() as RenderBox;
            //获取全局坐标
            print(box.localToGlobal(Offset.zero));
            //获取 Widget
            var w = key.currentWidget!;
            print(w.runtimeType);
            //获取 context
            var context = key.currentContext!;
            print('${context.size} size');
            //换颜色
            key.currentState?.changeColor();
          },
          child: const Text('换颜色')),
   ))
复制代码

通过 GlobalKey 找到 element 的逻辑是这样的。

在 Element mount 的时候先注册 key

void mount(Element? parent, Object? newSlot) {
 ...
  final Key? key = widget.key;
    if (key is GlobalKey) {
      owner!._registerGlobalKey(key, this);
  }
}
复制代码

其实注册就是加到 owner的 Map里

final Map<GlobalKey, Element> _globalKeyRegistry = <GlobalKey, Element>{};
复制代码

用的时候根据注册的 key 找到 element

Element? get _currentElement => WidgetsBinding.instance.buildOwner!._globalKeyRegistry[this];
复制代码

找到了 element 就找到了 currentContext ,因为 contenxt 就是 element ,通过 element 还能找到 widget 和 state。

GlobalKey currentState 属性返回的是范型, GlobalKey 的范型声明就是给它准备的。

T? get currentState;
复制代码

用完之后,在 Element 的 unmount 方法里会删除注册

void unmount() {
    ...
    final Key? key = _widget?.key;
    if (key is GlobalKey) {
      owner!._unregisterGlobalKey(key, this);
    }
    ...
  }
复制代码

有人不敢用 GlobalKey,理由是 GlobalKey 的成本很高。我们了解了 GlobalKey 找到 element 整个过程,并没有成本高的地方。所以在需要的地方,放心用!

二、在层级改变时保持状态

完整代码地址gitee.com/iam17/flutt…

image.png


在有globalkey 的情况下,Box 状态保持,点重新build按钮,Box的颜色不变。现在我们去掉 globalkey

Builder(builder: (BuildContext context) {
        _count++;
        if (_count % 2 == 0) {
          return Box(
            // 去掉 glokeyKey 看看效果
           // key: _globalKey,
          );
        } else {
          return Center(
            child: Box(
              // 去掉 glokeyKey 看看效果
              //key: _globalKey,
            ),
          );
        }
    })
复制代码

去掉 globalkey 之后,我们发现,每次 build,颜色都会改变,说明状态丢失,丢失的原因是,每次 Box 都会重建。

所谓的状态保持其实就是 Box 对应的 element 用 update 代替了 create,所以 state 才能保持。

所以在同父级下更新 widget,可以用 localKey,不同父级下更新 widget,可以用 globalkey。

相比较而言,通过 GlobalKey 移动子树的成本会高一些,但是相比于重建子树,GlobalKey 的方案开销还是要小。

最后回答一下前面提到的问题:如果交换 Box 的同时,同时修改 text,会走element 更新逻辑的第几条?

答案是走 第 4 条逻辑。

目录
相关文章
|
XML Java Android开发
Flutter | Key的原理和使用(上)
Flutter | Key的原理和使用(上)
Flutter | Key的原理和使用(上)
|
Android开发 容器
Flutter | Key的原理和使用(下)
Flutter | Key的原理和使用(下)
|
存储 Android开发 iOS开发
【Flutter Widget】Flutter移动UI框架使用Material和密匙Key的具体在项目里的实战经验
【Flutter Widget】Flutter移动UI框架使用Material和密匙Key的具体在项目里的实战经验
|
3月前
|
开发框架 前端开发 测试技术
Flutter开发常见问题解答
Flutter开发常见问题解答
|
4月前
|
前端开发 C++ 容器
Flutter-完整开发实战详解(一、Dart-语言和-Flutter-基础)(1)
Flutter-完整开发实战详解(一、Dart-语言和-Flutter-基础)(1)
|
4天前
|
JSON Dart Java
flutter开发多端平台应用的探索
flutter开发多端平台应用的探索
19 6
|
4天前
|
JSON Dart Java
flutter开发多端平台应用的探索 下 (跨模块、跨语言通信之平台通道)
flutter开发多端平台应用的探索 下 (跨模块、跨语言通信之平台通道)
|
7天前
|
安全 Android开发 开发者
探索安卓开发的未来:Kotlin的崛起与Flutter的挑战
在移动开发的广阔天地中,安卓平台始终占据着举足轻重的地位。随着技术的不断进步和开发者需求的多样化,Kotlin和Flutter成为了改变游戏规则的新玩家。本文将深入探讨Kotlin如何以其现代化的特性赢得开发者的青睐,以及Flutter凭借跨平台的能力如何挑战传统的安卓开发模式。通过实际案例分析,我们将揭示这两种技术如何塑造未来的安卓应用开发。
23 6
|
24天前
|
开发框架 Android开发 iOS开发
Flutter相关痛点解决问题之淘特选择桌面端开发框架如何解决
Flutter相关痛点解决问题之淘特选择桌面端开发框架如何解决
|
1月前
|
移动开发 前端开发 JavaScript
"跨界大战!React Native、Weex、Flutter:三大混合开发王者正面交锋,揭秘谁才是你移动应用开发的终极利器?"
【8月更文挑战第12天】随着移动应用开发的需求日益增长,高效构建跨平台应用成为关键。React Native、Weex与Flutter作为主流混合开发框架各具特色。React Native依托Facebook的强大支持,以接近原生的性能和丰富的组件库著称;Weex由阿里巴巴开发,性能优越尤其在大数据处理上表现突出;Flutter则凭借Google的支持及独特的Dart语言和Skia渲染引擎,提供出色的定制能力和开发效率。选择时需考量项目特性、团队技能及生态系统的成熟度。希望本文对比能助你做出最佳决策。
95 1