Flutter | Key的原理和使用(上)

简介: Flutter | Key的原理和使用(上)

概述


在几乎所有的 widget 中,都有一个参数 key ,那么这个 key 的作用是什么,在什么时候才需要使用到 key ?


没有 key 会出现什么问题?


我们直接看一个计数器的例子:


class Box extends StatefulWidget {
  final Color color;
  Box(this.color);
  @override
  _BoxState createState() => _BoxState();
}
class _BoxState extends State<Box> {
  int _count = 0;
  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      child: Container(
          width: 100,
          height: 100,
          color: widget.color,
          alignment: Alignment.center,
          child: Text(_count.toString(), style: TextStyle(fontSize: 30))),
      onTap: () => setState(() => ++_count),
    );
  }
}


Row(
  mainAxisAlignment: MainAxisAlignment.center,
  children: <Widget>[
    Box(Colors.blue),
    Box(Colors.red),
  ],
)


可以看到上图中蓝色的数字时三,而红色的是 5,接着修改代码,将蓝色和红色的位置互换,然后热重载一下,如下:


Row(
  mainAxisAlignment: MainAxisAlignment.center,
  children: <Widget>[
    Box(Colors.red),
    Box(Colors.blue),
  ],
),


接着就会发现,颜色已经互换了,但是数字并没有发生改变,


这时,我们在后面新添加一个红色,


接着在删除第一个带有数字 3 的红色,按道理来说应该就会剩下 5,0,


但是你会发现结果依旧是 3,5。


在这个示例中 flutter 不能通过 Container 的颜色来设置标识,所以就没办法确定那个到底是哪个,所以我们需要一个类似于 id 的东西,给每个 widget 一个标识,而 key 就是这个标识。


接着我们修改一下上面的示例:


class Box extends StatefulWidget {
  final Color color;
  Box(this.color, {Key key}) : super(key: key);
  @override
  _BoxState createState() => _BoxState();
}


Row(
  mainAxisAlignment: MainAxisAlignment.center,
  children: <Widget>[
    Box(Colors.blue, key: ValueKey(1)),
    Box(Colors.red, key: ValueKey(2)),
  ],
)


在代码中添加了 key,然后就会发现已经没有上面的问题了。但是如果我们给 Box 在包裹一层 Container,然后在次热重载的时候,数字都变成了 0,在去掉 Container 后数字也会变成 0,具体的原因我们在后面说;


Widget 和 Element 的对应关系


widget 的定义就是 对一个 Element 配置的描述,也就是说,widget 只是一个配置的描述,并不是真正的渲染对象,就相当于是 Android 里面的 xml,只是描述了一下属性,但他并不是真正的 View。并且通过查看源码可知 widget 中有一个 createElement 方法,用来创建 Element。


而 Element 则就是 Widget 树 中特定位置对应的实例,如下图所示:


0a2653c851af460fa595bd959398a8f1.png


上图刚好对应上面的例子:


**在没有 key 的情况下,**如果替换掉 第一个和第二个 box 置换,那么第二个就会使用第一个 box 的 Element,所以他的状态不会发生改变,但是因为颜色信息是在 widget 上的,所以颜色就会改变。最终置换后结果就是颜色改变了,但是里面的值没有发生变化。


又或者删除了第一个 box,第二个box 就会使用第一个 boxElement 的状态,所以说也会有上面的问题。


加上 key 的情况:


加上 key 之后,widget 和 element 会有对应关系,如果 key 没有对应就会重新在同层级下寻找,如果没有最终这个 widget 或者 Element 就会被删除


解释一下上面遗留的问题


在 Box 外部嵌套 Container 之后状态就没有了。这是因为 判断 key 之前首先会判断类型是否一致,然后在判断 key 是否相同。


正因为类型不一致,所以之前的 State 状态都无法使用,所以就会重新创建一个新的。


需要注意的是,继承自 StatelessWidget 的 Widget 是不需要使用 Key 的,因为它本身没有状态,不需要用到 Key。


键在具有相同父级的 [Element] 中必须是唯一的。相比之下,[GlobalKey] 在整个应用程序中必须是唯一的。另请参阅:[Widget.key],其中讨论了小部件如何使用键。


LocalKey 的三种类型


LocalKey 继承自 Key, 翻译过来就是局部键,LocalKey 在具有相同父级的 Element 中必须是惟一的。也就是说,LocalKey 在同一层级中必须要有唯一性。


LocalKey 有三种子类型,下面我们来看一下:


ValueKey
class ValueKey<T> extends LocalKey {
  final T value;
  const ValueKey(this.value);
  @override
  bool operator ==(Object other) {
    if (other.runtimeType != runtimeType)
      return false;
    return other is ValueKey<T>
        && other.value == value;
  }
}


使用特定类型的值来标识自身的键,ValueKey 在最上面的例子中已经使用过了,他可以接收任何类型的一个对象来最为 key。


通过源码我们可以看到它重写了 == 运算符,在判断是否相等的时候首先判断了类型是否相等,然后再去判断 value 是否相等;


ObjectKey
class ObjectKey extends LocalKey {
  const ObjectKey(this.value);
  final Object? value;
  @override
  bool operator ==(Object other) {
    if (other.runtimeType != runtimeType)
      return false;
    return other is ObjectKey
        && identical(other.value, value);
  }
  @override
  int get hashCode => hashValues(runtimeType, identityHashCode(value));
}


ObjectKey 和 ValueKey 最大的区别就是比较的算不一样,其中首先也是比较的类型,然后就调用 indentical 方法进行比较,其比较的就是内存地址,相当于 java 中直接使用 == 进行比较。而 LocalKey 则相当于 java 中的 equals 方法用来比较值的。


需要注意的是使用 ValueKey 中使用 == 比较的时候,如果没有重写 hashCode 和 == ,那样即使 对象的值是相等的,但比较出来也是不相等的。所以说尽量重写吧!


UniqueKey
class UniqueKey extends LocalKey {
  UniqueKey();
}


很明显,从名字中可以看出来,这是一个独一无二的 key。


每次重新 build 的时候,UniqueKey 都是独一无二的,所以就会导致无法找到对应的 Element,状态就会丢失。那么在什么时候需要用到这个 UniqueKey呢?我们可以自行思考一下。


还有一种做法就是把 UniqueKey 定义在 build 的外面,这样就不会出现状态丢失的问题了。


GlobalKey


GlobalKey 继承自 Key,相比与 LocalKey,他的作用域是全局的,而 LocalKey 只作用于当前层级。


在之前我们遇到一个问题,就是如果给一个 Widget 外面嵌套了一层,那么这个 Widget 的状态就会丢失,如下:


children: <Widget>[
    Box(Colors.red),
    Box(Colors.blue),
  ],
 ///修改为如下,然后重新 build
  children: <Widget>[
    Box(Colors.red),
    Container(child:Box(Colors.blue)),
  ],


原因在之前我们也讲过,就是因为类型不同。只有在类型和 key 相同的时候才会保留状态 ,显然上面的类型是不相同的;


那么遇到这种问题要怎么办呢,这个时候就可以使用 GlobalKey 了。我们看下面的栗子:


class Counter extends StatefulWidget {
  Counter({Key key}) : super(key: key);
  @override
  _CounterState createState() => _CounterState();
}
class _CounterState extends State<Counter> {
  int _count = 0;
  @override
  Widget build(BuildContext context) {
    return RaisedButton(
      onPressed: () => setState(() => _count++),
      child: Text("$_count", style: TextStyle(fontSize: 70)),
    );
  }
}


final _globalKey = GlobalKey();
@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      title: Text(widget.title),
    ),
    body: Center(
      child: Row(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          Counter(),
          Counter(),
        ],
      ),
    ),
  );
}


上面代码中,我们定义了一个 Counter 组件,点击后 count 自增,和一个 GlobakKey 的对象。


接着我们点击 Counter 组件,自增之后,给 Counter 包裹一层 Container 之后进行热重载,就会发现之前自增的数字已经不见了。这个时候我们还没有使用 GlobalKey。


接着我们使用 GlobalKey,如下


Row(
     mainAxisAlignment: MainAxisAlignment.center,
     children: <Widget>[
         Counter(),
         Counter(key: _globalKey),
     ],
   ),
 )


重新运行,并且点击自增,运行效果如下:


0a2653c851af460fa595bd959398a8f1.png


接着我们来修改一下代码:


Column(
  mainAxisAlignment: MainAxisAlignment.center,
  children: <Widget>[
    Counter(),
    Container(child: Counter(key: _globalKey)),
  ],
),


我们将最外层的 Row 换成了 Column,并且给最后一个 Counter 包裹了一个 Container 组件,猜一下结果会如何??,我们来看一下结果:


2d65d23f6d4748949b924e4057485923.png


结果就是 Column 已经生效了,使用了 GlobalKey 的 Counter 状态没有被清除,而上面这个没有使用的则没有了状态。


我们简单的分析一下,热重载的时候回重新 build 一下,执行到 Column 位置的时候发现之前的类型是 Row,然后之前 Row 的 Element 就会被扔掉,重新创建 Element。Row 的 Element 扔掉之后,其内部的所有状态也都会消失,但是到了最里面的 Counter 的时候,就会根据 Counter 的 globalkey 重新查找对应的状态,找到之后就会继续使用。


相关文章
|
17天前
|
开发者 容器
Flutter&鸿蒙next 布局架构原理详解
本文详细介绍了 Flutter 中的主要布局方式,包括 Row、Column、Stack、Container、ListView 和 GridView 等布局组件的架构原理及使用场景。通过了解这些布局 Widget 的基本概念、关键属性和布局原理,开发者可以更高效地构建复杂的用户界面。此外,文章还提供了布局优化技巧,帮助提升应用性能。
78 4
|
17天前
|
存储 Dart 前端开发
flutter鸿蒙版本mvvm架构思想原理
在Flutter中实现MVVM架构,旨在将UI与业务逻辑分离,提升代码可维护性和可读性。本文介绍了MVVM的整体架构,包括Model、View和ViewModel的职责,以及各文件的详细实现。通过`main.dart`、`CounterViewModel.dart`、`MyHomePage.dart`和`Model.dart`的具体代码,展示了如何使用Provider进行状态管理,实现数据绑定和响应式设计。MVVM架构的分离关注点、数据绑定和可维护性特点,使得开发更加高效和整洁。
146 3
|
23天前
动画控制器在 Flutter 中的工作原理
【10月更文挑战第18天】总的来说,动画控制器 `AnimationController` 在 Flutter 中起着关键的作用,它通过控制动画的数值、速度、节奏和状态,实现了丰富多彩的动画效果。理解它的工作原理对于我们在 Flutter 中创建各种精彩的动画是非常重要的。
|
29天前
|
容器
Flutter&鸿蒙next 布局架构原理详解
Flutter&鸿蒙next 布局架构原理详解
|
5月前
|
存储 开发框架 JavaScript
深入探讨Flutter中动态UI构建的原理、方法以及数据驱动视图的实现技巧
【6月更文挑战第11天】Flutter是高效的跨平台移动开发框架,以其热重载、高性能渲染和丰富组件库著称。本文探讨了Flutter中动态UI构建原理与数据驱动视图的实现。动态UI基于Widget树模型,状态变化触发UI更新。状态管理是关键,Flutter提供StatefulWidget、Provider、Redux等方式。使用ListView等可滚动组件和StreamBuilder等流式组件实现数据驱动视图的自动更新。响应式布局确保UI在不同设备上的适应性。Flutter为开发者构建动态、用户友好的界面提供了强大支持。
99 2
|
1月前
flutter:定时器&通过key操作状态 (十二)
本文档介绍了Flutter中的定时器使用方法、通过key操作状态的几种方式,包括localKey和GlobalKey的使用场景与示例代码,以及如何处理屏幕旋转导致的组件状态丢失问题。通过配置全局key,可以有效地管理父子组件之间的状态交互,确保在屏幕旋转等情况下保持组件状态的一致性。
|
4月前
|
Dart JavaScript Java
flutter 架构、渲染原理、家族
flutter 架构、渲染原理、家族
86 3
|
6月前
|
Android开发
Flutter完整开发实战详解(六、 深入Widget原理),2024百度Android岗面试真题收录解析
Flutter完整开发实战详解(六、 深入Widget原理),2024百度Android岗面试真题收录解析
|
6月前
|
开发框架 Dart API
Flutter引擎工作原理:深入解析FlutterEngine
【4月更文挑战第26天】FlutterEngine是Flutter应用的关键,负责Dart代码转换为原生代码,管理应用生命周期、渲染和事件处理。它初始化Flutter运行时环境,加载并编译Dart代码,创建渲染树,处理事件并实现跨平台兼容。通过理解其工作原理,开发者能更好地掌握Flutter应用内部机制并优化开发。随着Flutter生态系统发展,FlutterEngine将持续提供强大支持。
|
6月前
|
Web App开发 前端开发 iOS开发
CSS3 转换,深入理解Flutter动画原理,前端基础图形
CSS3 转换,深入理解Flutter动画原理,前端基础图形