1662 字
8 分钟
Key
2021-12-02
2026-10-12
无标签

Key 的作用#

我们创建这样一个示例:

import 'dart:math';
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: HomePage(),
);
}
}
class HomePage extends StatefulWidget {
const HomePage({Key? key}) : super(key: key);
@override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
//key的作用就非常大了!!
List<Widget> items = [
ColorItem('第1个'),
ColorItem('第2个'),
ColorItem('第3个'),
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Key的作用'),
),
body: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: items,
),
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.add),
onPressed: () {
setState(() {
items.removeAt(0);
});
},
),
);
}
}
class ColorItem extends StatefulWidget {
final String title;
ColorItem(this.title, {Key? key}) : super(key: key);
@override
_ColorItemState createState() => _ColorItemState();
}
class _ColorItemState extends State<ColorItem> {
final color = Color.fromRGBO(
Random().nextInt(256), Random().nextInt(256), Random().nextInt(256), 1.0);
@override
Widget build(BuildContext context) {
return Container(
width: 100,
height: 100,
child: Text(widget.title),
color: color,
);
}
}

1

运行发现了上面的现象,作为有 iOS 开发经验的我们,很容易联想到这应该是和复用有关的现象。

复用问题#

这里我们需要理解, ColorItem 作为一个 StatefulWidget 它是由两部分组成的: Widget 和 State 。这里就是 Widget 被移除后, State 还在内存中。所以出现了复用异常的问题。

那么将数据放在 Widget 中按照这里分析的理论来说应该就没有这个现象了吧?我们试试:

class ColorItem extends StatefulWidget {
final String title;
// 移到 Widget 中
final color = Color.fromRGBO(
Random().nextInt(256), Random().nextInt(256), Random().nextInt(256), 1.0);
ColorItem(this.title, {Key? key}) : super(key: key);
@override
_ColorItemState createState() => _ColorItemState();
}
class _ColorItemState extends State<ColorItem> {
@override
Widget build(BuildContext context) {
return Container(
width: 100,
height: 100,
child: Text(widget.title),
color: widget.color,
);
}
}

2

验证成功!那么具体原因是什么呢?

增量渲染与 canUpdate#

我们来到 Widget 的实现中, canUpdate 方法决定了一个 Widget 的 Element 是否会被更新,而 Element 的更新,又直接关系到了增量渲染。

同时满足两个条件:

  • 新旧 Widget 的类型相同
  • 新旧 Widget 的 Key 相同
    • 如果 Key 是空的,只通过类型判断,就算他们的子 Widget 完全不同

这里就很好的解释了一开始发生的异常现象。

/// Whether the `newWidget` can be used to update an [Element] that currently
/// has the `oldWidget` as its configuration.
///
/// An element that uses a given widget as its configuration can be updated to
/// use another widget as its configuration if, and only if, the two widgets
/// have [runtimeType] and [key] properties that are [operator==].
///
/// If the widgets have no key (their key is null), then they are considered a
/// match if they have the same type, even if their children are completely
/// different.
static bool canUpdate(Widget oldWidget, Widget newWidget) {
return oldWidget.runtimeType == newWidget.runtimeType
&& oldWidget.key == newWidget.key;
}

图解#

Widget 和 Element 是一一对应的,而 State 是在 Element 中的。

4

  • 移除一个 Widget 开始检查
  • 第一个 Element 检查到第二个 Widget
    • 调用 canUpdate
      • 类型相同
      • Key 为空
      • return ture
    • 于是就用 第一个Element 更新 第二个Widget
  • 依次类推,就发生了前面的异常现象

5

既然这里提到了 Key ,那么加上 Key 是否就可以上面这个问题呢?

Key 的使用#

这里为每个 ColorItem 加上 Key , 并将 Color 属性放回 State 中:

...
class _HomePageState extends State<HomePage> {
//key的作用就非常大了!!
List<Widget> items = [
ColorItem(
'第1个',
key: ValueKey(1),
),
ColorItem(
'第2个',
key: ValueKey(2),
),
ColorItem(
'第3个',
key: ValueKey(3),
),
];
@override
Widget build(BuildContext context) {
...
}
}
class ColorItem extends StatefulWidget {
final String title;
...
}
class _ColorItemState extends State<ColorItem> {
final color = Color.fromRGBO(
Random().nextInt(256), Random().nextInt(256), Random().nextInt(256), 1.0);
...
}

3

有效!

Key#

Key 本身是一个抽象类,有一个工厂方法:

/// A [Key] is an identifier for [Widget]s, [Element]s and [SemanticsNode]s.
///
/// A new widget will only be used to update an existing element if its key is
/// the same as the key of the current widget associated with the element.
///
/// {@youtube 560 315 https://www.youtube.com/watch?v=kn0EOS-ZiIc}
///
/// Keys must be unique amongst the [Element]s with the same parent.
///
/// Subclasses of [Key] should either subclass [LocalKey] or [GlobalKey].
///
/// See also:
///
/// * [Widget.key], which discusses how widgets use keys.
@immutable
abstract class Key {
/// Construct a [ValueKey<String>] with the given [String].
///
/// This is the simplest way to create keys.
const factory Key(String value) = ValueKey<String>;
/// Default constructor, used by subclasses.
///
/// Useful so that subclasses can call us, because the [new Key] factory
/// constructor shadows the implicit constructor.
@protected
const Key.empty();
}
  • 它有两个子类
    • LocalKey
      • 同一个 父Element 内唯一的
    • GlobalKey
      • 整个 App 内唯一

刚才用的 ValueKey 是 LocalKey 的子类。

LocalKey#

区别哪个 Element 要保留,哪个 Element 要删除。

/// A key that is not a [GlobalKey].
///
/// Keys must be unique amongst the [Element]s with the same parent. By
/// contrast, [GlobalKey]s must be unique across the entire app.
///
/// See also:
///
/// * [Widget.key], which discusses how widgets use keys.
abstract class LocalKey extends Key {
/// Abstract const constructor. This constructor enables subclasses to provide
/// const constructors so that they can be used in const expressions.
const LocalKey() : super.empty();
}
  • ValueKey
    • 以值作为参数(数字、字符串等)
  • ObjectKey
    • 以对象作为参数
  • UniqueKey
    • 创建唯一标识

GlobalKey#

在整个 App 中 唯一的Key。 GlobalKey 可以唯一标识一个元素, 比如访问一个 BuildContext 或者一个 Widget。对于 StatefulWidget 而言, GlobalKey 也可以访问 State 。

当有 GlobalKey 的 Widget 被移动到 Widget树 中新的位置的话,会重新渲染他们的子树。

为了渲染子树,一个 Widget 必须在同一个动画帧内完成在树中,从旧位置移动到新位置。

重新渲染一个使用 GlobalKey 的 Element 是很消耗性能的,因为会触发调用所有相关的 State 的 deactivate 方法,然后让所有依赖 InheritedWidget 的 Widget 重建。

所以如果你不需要达到上面的效果,那么建议使用其他的 Key 。

注意点#

  • 两个在同一个树中的 Widget 不能同时有相同的 GlobalKey 。尝试这样做的话在运行时会触发异常。
  • GlobalKeys 不应该再每次 build 的时候被重新创建。他们应该是长期被一个 State 所持有的。
    • 例如:
      • 每次 build 的时候都创建一个新的 GlobalKey 会丢弃和 旧Key 相关的子树,并为新Key创建一个新树。除了会损害性能,这种操作还有可能会对子树造成未知的影响。
      • 例如子树中的 GestureDetector 将无法继续追踪正在进行中的手势,因为它会在每次 build 的时候被重新创建
  • 比较好的做法是:
    • 让一个 State 持有这个 GlobalKey ,并且在 Build 方法外初始化它
    • 比如在 State.initState 中

GlobalKey简单示例#

import 'dart:math';
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: GlobalKeyDemo(),
);
}
}
class GlobalKeyDemo extends StatelessWidget {
final GlobalKey<_GKeyItemState> _gKey = GlobalKey();
GlobalKeyDemo({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Key的作用'),
),
body: Center(
child: GKeyItem(key: _gKey,),
),
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.add),
onPressed: () {
_gKey.currentState?.setState(() {
_gKey.currentState?.count += 1;
});
},
),
);
}
}
class GKeyItem extends StatefulWidget {
const GKeyItem({Key? key}) : super(key: key);
@override
_GKeyItemState createState() => _GKeyItemState();
}
class _GKeyItemState extends State<GKeyItem> {
int count = 0;
@override
Widget build(BuildContext context) {
return Center(
child: Text('$count'),
);
}
}