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 条逻辑。

目录
相关文章
|
1月前
flutter:定时器&通过key操作状态 (十二)
本文档介绍了Flutter中的定时器使用方法、通过key操作状态的几种方式,包括localKey和GlobalKey的使用场景与示例代码,以及如何处理屏幕旋转导致的组件状态丢失问题。通过配置全局key,可以有效地管理父子组件之间的状态交互,确保在屏幕旋转等情况下保持组件状态的一致性。
|
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的具体在项目里的实战经验
|
1月前
|
Android开发 iOS开发 容器
鸿蒙harmonyos next flutter混合开发之开发FFI plugin
鸿蒙harmonyos next flutter混合开发之开发FFI plugin
|
1月前
|
开发者
鸿蒙Flutter实战:07-混合开发
鸿蒙Flutter混合开发支持两种模式:1) 基于har包,便于主项目开发者无需关心Flutter细节,但不支持热重载;2) 基于源码依赖,利于代码维护与热重载,需配置Flutter环境。项目结构包括AppScope、flutter_module等目录,适用于不同开发需求。
74 3
|
17天前
|
传感器 开发框架 物联网
鸿蒙next选择 Flutter 开发跨平台应用的原因
鸿蒙(HarmonyOS)是华为推出的一款旨在实现多设备无缝连接的操作系统。为了实现这一目标,鸿蒙选择了 Flutter 作为主要的跨平台应用开发框架。Flutter 的跨平台能力、高性能、丰富的生态支持和与鸿蒙系统的良好兼容性,使其成为理想的选择。通过 Flutter,开发者可以高效地构建和部署多平台应用,推动鸿蒙生态的快速发展。
130 0
|
19天前
|
Dart 安全 UED
Flutter&鸿蒙next中的表单封装:提升开发效率与用户体验
在移动应用开发中,表单是用户与应用交互的重要界面。本文介绍了如何在Flutter中封装表单,以提升开发效率和用户体验。通过代码复用、集中管理和一致性的优势,封装表单组件可以简化开发流程。文章详细讲解了Flutter表单的基础、封装方法和表单验证技巧,帮助开发者构建健壮且用户友好的应用。
60 0
|
1月前
|
开发框架 移动开发 Android开发
安卓与iOS开发中的跨平台解决方案:Flutter入门
【9月更文挑战第30天】在移动应用开发的广阔舞台上,安卓和iOS两大操作系统各自占据半壁江山。开发者们常常面临着选择:是专注于单一平台深耕细作,还是寻找一种能够横跨两大系统的开发方案?Flutter,作为一种新兴的跨平台UI工具包,正以其现代、响应式的特点赢得开发者的青睐。本文将带你一探究竟,从Flutter的基础概念到实战应用,深入浅出地介绍这一技术的魅力所在。
77 7
|
1月前
|
编解码 Dart API
鸿蒙Flutter实战:06-使用ArkTs开发Flutter鸿蒙插件
本文介绍了如何开发一个 Flutter 鸿蒙插件,实现 Flutter 与鸿蒙的混合开发及双端消息通信。通过定义 `MethodChannel` 实现 Flutter 侧的 token 存取方法,并在鸿蒙侧编写 `EntryAbility` 和 `ForestPlugin`,使用鸿蒙的首选项 API 完成数据的读写操作。文章还提供了注意事项和参考资料,帮助开发者更好地理解和实现这一过程。
57 0