23.Key
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,
);
}
}

运行发现了上面的现象,作为有 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,
);
}
}

验证成功!那么具体原因是什么呢?
增量渲染与 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
中的。

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

既然这里提到了 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);
...
}

有效!
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'),
);
}
}
Last updated
Was this helpful?