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?