在Flutter 中,Key 是 几乎所有 widget 都具有的属性。为什么 widget 具有 Key 呢?Key的作用是什么?
什么是 Key
Key是Widget、Element 和 SemanticNodes 的标识符。 Key 是Widget、Element 和 SemanticNodes的唯一标识。例如对于 Widget 在 Widget 树中改变了位置,Key 可以帮助它们保留状态。相对于无状态的Widget,Key对于有状态的 Widget 作用更大。
Key的使用场景
在添加、删除或重排同一类型的 widget 集合时,Key 可以让这些 widget 保持状态,并且在 widget 树中处于相同的级别。
例子:两个颜色块单击按钮时交换两者位置。
两种实现方式:
第一种实现:widget 是无状态的,色值保存在 widget 本身中。当点击 FloatingActionButton,色块会交换位置。
import 'dart:math';
import 'package:flutter/material.dart';
//色块widget是无状态的
class StatelessColorTitles extends StatelessWidget {
//色值保存在本身控件中
var r = Random().nextInt(256);
var g = Random().nextInt(256);
var b = Random().nextInt(256);
@override
Widget build(BuildContext context) {
return Container(
height: 100,
width: 100,
color: Color.fromRGBO(r, g, b, 1),
);
}
}
class PositionTiles extends StatefulWidget {
const PositionTiles({Key? key}) : super(key: key);
@override
State<StatefulWidget> createState() {
return _PositionTilesState();
}
}
class _PositionTilesState extends State<PositionTiles> {
List<Widget> titles = [];
@override
void initState() {
super.initState();
titles = [StatelessColorTitles(), StatelessColorTitles()];
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: titles,
),
),
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.sentiment_very_satisfied),
onPressed: swapTitles,
),
);
}
void swapTitles() {
setState(() {
titles.insert(1, titles.removeAt(0));
});
}
}
class MyCustomApp extends StatelessWidget {
const MyCustomApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(primarySwatch: Colors.blue),
home: const PositionTiles(),
);
}
}
下面是第二种实现:色块 widget 有状态,色值保存在状态中。为了正确交换平铺位置,我们需要向有状态的 widget 添加 key 参数。
import 'dart:math';
import 'package:flutter/material.dart';
class _StatefulColorTilesState2 extends State<StatefulColorTiles2> {
//色值保存在本身控件中
var r = Random().nextInt(256);
var g = Random().nextInt(256);
var b = Random().nextInt(256);
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
return Container(
height: 100,
width: 100,
color: Color.fromRGBO(r, g, b, 1),
);
}
}
class StatefulColorTiles2 extends StatefulWidget {
const StatefulColorTiles2({Key? key}) : super(key: key);
@override
State<StatefulWidget> createState() {
return _StatefulColorTilesState2();
}
}
class PositionTiles2 extends StatefulWidget {
const PositionTiles2({Key? key}) : super(key: key);
@override
State<StatefulWidget> createState() {
return _PositionTilesState2();
}
}
class _PositionTilesState2 extends State<PositionTiles2> {
List<Widget> titles = [];
@override
void initState() {
super.initState();
titles = [
//添加了key参数,若不添加则点击按钮色块不会交互
StatefulColorTiles2(key: UniqueKey()),
StatefulColorTiles2(key: UniqueKey())
];
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: titles,
)),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.sentiment_very_satisfied),
onPressed: swapTitles,
),
);
}
void swapTitles() {
setState(() {
titles.insert(1, titles.removeAt(0));
});
}
}
class MyCustomApp2 extends StatelessWidget {
const MyCustomApp2({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: "FLutter Demo about key",
theme: ThemeData(primarySwatch: Colors.blue),
home: const PositionTiles2());
}
}
widget 有状态时,才需要设置 key。如果是无状态的 widget 则可以不需要设置 key。
原理
渲染 widget 时,Flutter 在构建 widget 树的同时,还会构建其对应的Element树。Element树持有 widget 树中 widget 的信息及其子 widget 的引用。在修改和重新渲染的过程中,Flutter 查找Element树查看其是否改变,以便在元素未改变时可以复用旧元素。
注意:
1.widget 树封装配置信息,Element树相当于实例对象。widget 类似于 json串,Element树类似于 json 解析后的 bean。
2. widget 类型 和 key 值 ,在没用 key 的情况下,类型相同表示新旧 widget 可复用。
#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;
}
在上面实现方式一的例子中:原色块是 一 和 二, 交换后 oldWidget 与 newWidget 比较,因为 一 和 二 是同类型 StatelessColorTiles ,则表示原来在Element树中的 Element(一)元素在交换后是可以继续供 Element(二)元素 复用。
键类型
Key 是抽象类,有两个重要的子类 LocalKey和 GlobalKey。
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 [Key.new] factory
/// constructor shadows the implicit constructor.
@protected
const Key.empty();
}
Localkey
LocalKey 是 Key 的子类,用于标识局部范围内的 Widgets。它有两个具体实现:ValueKey 和 ObjectKey。
/// 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
ValueKey 是简单的值(如字符串、数字)来标识 Widget。它通常用于具有唯一标识的对象。
/// A key that uses a value of a particular type to identify itself.
///
/// A [ValueKey<T>] is equal to another [ValueKey<T>] if, and only if, their
/// values are [operator==].
///
/// This class can be subclassed to create value keys that will not be equal to
/// other value keys that happen to use the same value. If the subclass is
/// private, this results in a value key type that cannot collide with keys from
/// other sources, which could be useful, for example, if the keys are being
/// used as fallbacks in the same scope as keys supplied from another widget.
///
/// See also:
///
/// * [Widget.key], which discusses how widgets use keys.
class ValueKey<T> extends LocalKey {
/// Creates a key that delegates its [operator==] to the given value.
const ValueKey(this.value);
/// The value to which this key delegates its [operator==]
final T value;
@override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType) {
return false;
}
return other is ValueKey<T>
&& other.value == value;
}
@override
int get hashCode => Object.hash(runtimeType, value);
@override
String toString() {
final String valueString = T == String ? "<'$value'>" : '<$value>';
// The crazy on the next line is a workaround for
// https://github.com/dart-lang/sdk/issues/33297
if (runtimeType == _TypeLiteral<ValueKey<T>>().type) {
return '[$valueString]';
}
return '[$T $valueString]';
}
}
Container(
alignment: Alignment.centerLeft,
width: 100,
height: 70,
key: const ValueKey("TipItem")
)
ObjectKey
ObjectKey 使用对象作为标识符。这对于需要使用对象来唯一标识 Widget 的情况非常有用。
ObjectKey跟ValueKey类似,只是value的类型从T变成了Object,operator方法也有变化。
identical对比是否是相等或者说相同的时候,它其实属于对比引用或者说指针是否相等,类似于Java中对比内存地址。
/// A key that takes its identity from the object used as its value.
///
/// Used to tie the identity of a widget to the identity of an object used to
/// generate that widget.
///
/// See also:
///
/// * [Key], the base class for all keys.
/// * The discussion at [Widget.key] for more information about how widgets use
/// keys.
class ObjectKey extends LocalKey {
/// Creates a key that uses [identical] on [value] for its [operator==].
const ObjectKey(this.value);
/// The object whose identity is used by this key's [operator==].
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 => Object.hash(runtimeType, identityHashCode(value));
@override
String toString() {
if (runtimeType == ObjectKey) {
return '[${describeIdentity(value)}]';
}
return '[${objectRuntimeType(this, 'ObjectKey')} ${describeIdentity(value)}]';
}
}
class Student {
final String name;
final int age;
Student(this.name, this.age);
@override
bool operator ==(Object other) {
return identical(this, other) ||
other is Student &&
runtimeType == other.runtimeType &&
name == other.name &&
age == other.age;
}
@override
int get hashCode => name.hashCode ^ age.hashCode;
}
Container(
alignment: Alignment.centerLeft,
width: 100,
height: 70,
key: ObjectKey(Student("Tom", 22)),
color: Colors.amber)
UniqueKey
UniqueKey:通过该对象可以生成一个具有唯一性的 hash 码。又因为每次 Widget 构建时都会重新生成一个新的 UniqueKey,失去了使用意义。(所以UniqueKey使用价值不高)。UniqueKey是一个独一无二的Key,比如UniqueKey()和UniqueKey()是不相等的。
/// A key that is only equal to itself.
///
/// This cannot be created with a const constructor because that implies that
/// all instantiated keys would be the same instance and therefore not be unique.
class UniqueKey extends LocalKey {
/// Creates a key that is equal only to itself.
///
/// The key cannot be created with a const constructor because that implies
/// that all instantiated keys would be the same instance and therefore not
/// be unique.
// ignore: prefer_const_constructors_in_immutables , never use const for this class
UniqueKey();
@override
String toString() => '[#${shortHash(this)}]';
}
PageStorageKey
PageStorageKey可以用于滑动列表,在列表页面通过某一个 Item的点击 跳转到了一个新的页面,当返回之前的列表页面时,列表的滑动的距离回到了顶部。此时给 Sliver 设置一个 PageStorageKey,就能够保持 Sliver 的滚动状态。
/// A [Key] that can be used to persist the widget state in storage after the
/// destruction and will be restored when recreated.
///
/// Each key with its value plus the ancestor chain of other [PageStorageKey]s
/// need to be unique within the widget's closest ancestor [PageStorage]. To
/// make it possible for a saved value to be found when a widget is recreated,
/// the key's value must not be objects whose identity will change each time the
/// widget is created.
///
/// See also:
///
/// * [PageStorage], which manages the data storage for widgets using
/// [PageStorageKey]s.
class PageStorageKey<T> extends ValueKey<T> {
/// Creates a [ValueKey] that defines where [PageStorage] values will be saved.
const PageStorageKey(super.value);
}
PageView + BottomNavigationBar 或者 TabBarView + TabBar 的时候发现当切换到另一页面的时候, 前一个页面就会被销毁, 再返回前一页时, 页面会被重建, 随之数据会重新加载, 控件会重新渲染 带来了极不好的用户体验。可以使用使用PageStorage在页面切换时保存状态。
import 'package:flutter/material.dart';
class Page1 extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return _Page1State();
}
}
class Page1Params {
int counter = 0;
}
class _Page1State extends State<Page1> {
Page1Params _params = Page1Params();
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(20.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
Text('${_params.counter}',
style: TextStyle(
fontSize: 50,
)),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
IconButton(
onPressed: () {
setState(() {
_params.counter--;
});
},
icon: Icon(Icons.remove, size: 32.0)),
IconButton(
onPressed: () {
setState(() {
_params.counter++;
});
},
icon: Icon(Icons.add, size: 32.0)),
],
)
],
),
);
}
}
class Page2 extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container();
}
}
class TabInfo {
String label;
Widget widget;
TabInfo(this.label, this.widget);
}
class MyCustomApp extends StatelessWidget {
final List<TabInfo> _tabs = [
TabInfo("FIRST", Page1()),
TabInfo("SECOND", Page2())
];
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: _tabs.length,
child: Scaffold(
appBar: AppBar(
title: Text('Tab Controller'),
bottom: PreferredSize(
child: TabBar(
isScrollable: true,
tabs: _tabs.map((TabInfo tab) {
return Tab(text: tab.label);
}).toList(),
),
preferredSize: Size.fromHeight(50.0),
),
),
body: TabBarView(
children: _tabs.map((tab) => tab.widget).toList(),
),
),
);
}
}
void main() => runApp(
MaterialApp(
debugShowCheckedModeBanner: false,
home: MyCustomApp(),
),
);
在上面的例子中,通过点击FIRST 和 SECOND 按钮切换页面可以发现来回切换以后,页面状态被清理了。
在上面的例子中,当出现页面切换的情况,每次标签栏切换页面时,之前的页面就被清理了。
出现上面问题的原因是,之前页面的状态(State)没有被保留下来,状态的reset导致页面发生了初始化。
通过使用PageStorage管理改造:
import 'package:flutter/material.dart';
class Page1 extends StatefulWidget {
//为Page1 添加一个带参数的构造函数,
//通过其将Key直接传给super,通过key可恢复状态
Page1({Key? key}) : super(key: key);
@override
State<StatefulWidget> createState() {
return _Page1State();
}
}
class Page1Params {
int counter = 0;
}
class _Page1State extends State<Page1> {
Page1Params _params = Page1Params();
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(20.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
Text('${_params.counter}',
style: TextStyle(
fontSize: 50,
)),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
IconButton(
onPressed: () {
//setState的同时通过writeState将状态保存进PageStorage。
setState(() {
_params.counter--;
});
PageStorage.of(context).writeState(context, _params);
},
icon: Icon(Icons.remove, size: 32.0)),
IconButton(
onPressed: () {
//setState的同时通过writeState将状态保存进PageStorage。
setState(() {
_params.counter++;
});
PageStorage.of(context).writeState(context, _params);
},
icon: Icon(Icons.add, size: 32.0)),
],
)
],
),
);
}
//didChangeDependencies会紧跟在initState之后被调用,便于进行state初始化
@override
void didChangeDependencies() {
//重写State类的didChangeDependencies方法,
//在里面通过readState从PageStorage读取并恢复被保存的状态。
var p = PageStorage.of(context).readState(context);
if (p != null) {
_params = p;
} else {
_params = Page1Params();
}
super.didChangeDependencies();
}
}
class Page2 extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container();
}
}
class TabInfo {
String label;
Widget widget;
TabInfo(this.label, this.widget);
}
class MyCustomApp extends StatelessWidget {
final List<TabInfo> _tabs = [
//创建widget时指定key
TabInfo("FIRST", Page1(key: PageStorageKey<String>("key_Page1"))),
TabInfo("SECOND", Page2())
];
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: _tabs.length,
child: Scaffold(
appBar: AppBar(
title: Text('Tab Controller'),
bottom: PreferredSize(
child: TabBar(
isScrollable: true,
tabs: _tabs.map((TabInfo tab) {
return Tab(text: tab.label);
}).toList(),
),
preferredSize: Size.fromHeight(50.0),
),
),
body: TabBarView(
children: _tabs.map((tab) => tab.widget).toList(),
),
),
);
}
}
void main() => runApp(
MaterialApp(
debugShowCheckedModeBanner: false,
home: MyCustomApp(),
),
);
上述代码示例中页面切换时通过PageStorage
保存并恢复状态。
GlobalKey
GlobalKey 适用于在应用程序的全局范围内唯一标识的 Widgets。GlobalKey不应该在每次build的时候重新重建, 它是State拥有的长期存在的对象。GlobalKey 能够跨 Widget 访问状态。
默认实现是LabeledGlobalKey,每次创建都是新的GlobalKey。GlobalKey都保存在BuildOwner类中的一个map里,map的key为GlobalKey,map的value则为GlobalKey关联的element。
/// A key that is unique across the entire app.
///
/// Global keys uniquely identify elements. Global keys provide access to other
/// objects that are associated with those elements, such as [BuildContext].
/// For [StatefulWidget]s, global keys also provide access to [State].
///
/// Widgets that have global keys reparent their subtrees when they are moved
/// from one location in the tree to another location in the tree. In order to
/// reparent its subtree, a widget must arrive at its new location in the tree
/// in the same animation frame in which it was removed from its old location in
/// the tree.
///
/// Reparenting an [Element] using a global key is relatively expensive, as
/// this operation will trigger a call to [State.deactivate] on the associated
/// [State] and all of its descendants; then force all widgets that depends
/// on an [InheritedWidget] to rebuild.
///
/// If you don't need any of the features listed above, consider using a [Key],
/// [ValueKey], [ObjectKey], or [UniqueKey] instead.
///
/// You cannot simultaneously include two widgets in the tree with the same
/// global key. Attempting to do so will assert at runtime.
///
/// ## Pitfalls
///
/// GlobalKeys should not be re-created on every build. They should usually be
/// long-lived objects owned by a [State] object, for example.
///
/// Creating a new GlobalKey on every build will throw away the state of the
/// subtree associated with the old key and create a new fresh subtree for the
/// new key. Besides harming performance, this can also cause unexpected
/// behavior in widgets in the subtree. For example, a [GestureDetector] in the
/// subtree will be unable to track ongoing gestures since it will be recreated
/// on each build.
///
/// Instead, a good practice is to let a State object own the GlobalKey, and
/// instantiate it outside the build method, such as in [State.initState].
///
/// See also:
///
/// * The discussion at [Widget.key] for more information about how widgets use
/// keys.
@optionalTypeArgs
abstract class GlobalKey<T extends State<StatefulWidget>> extends Key {
/// Creates a [LabeledGlobalKey], which is a [GlobalKey] with a label used for
/// debugging.
///
/// The label is purely for debugging and not used for comparing the identity
/// of the key.
factory GlobalKey({ String? debugLabel }) => LabeledGlobalKey<T>(debugLabel);
/// Creates a global key without a label.
///
/// Used by subclasses because the factory constructor shadows the implicit
/// constructor.
const GlobalKey.constructor() : super.empty();
Element? get _currentElement => WidgetsBinding.instance.buildOwner!._globalKeyRegistry[this];
/// The build context in which the widget with this key builds.
///
/// The current context is null if there is no widget in the tree that matches
/// this global key.
BuildContext? get currentContext => _currentElement;
/// The widget in the tree that currently has this global key.
///
/// The current widget is null if there is no widget in the tree that matches
/// this global key.
Widget? get currentWidget => _currentElement?.widget;
/// The [State] for the widget in the tree that currently has this global key.
///
/// The current state is null if (1) there is no widget in the tree that
/// matches this global key, (2) that widget is not a [StatefulWidget], or the
/// associated [State] object is not a subtype of `T`.
T? get currentState {
final Element? element = _currentElement;
if (element is StatefulElement) {
final StatefulElement statefulElement = element;
final State state = statefulElement.state;
if (state is T) {
return state;
}
}
return null;
}
}
一个泛型类型,T必须要继承自State<StatefulWidget>,可以说这个GlobalKey专门用于StatefulWidget组件。GlobalKey包含一个Map,key和value分别为自身和Element。GlobalKey会在组件Mount阶段把自身放到一个Map里面缓存起来。
GlobalKey的注意点:
- 当拥有GlobalKey的widget从树的一个位置上移动到另一个位置时,需要reparent它的子树。为了reparent它的子树,必须在一个动画帧里完成从旧位置移动到新位置的操作。
- 上面的reparent操作代价昂贵,因为要调用所有相关联的State和所有子节点的deactive方法,并且所有依赖InheritedWidget的widget去重建。
- 不要在build方法中创建GlobalKey,性能不好,容易出现异常。比如子树里的GestureDetector可能会由于每次build时重新创建GlobalKey而无法继续追踪手势事件。
- GlobalKey提供了访问其关联的Element和State的方法。
GlobalKey使用场景:
获取或者修改Widget的状态(State)
实现局部刷新
查找Widget
维持Widget的持久性
参考:
本站资源均来自互联网,仅供研究学习,禁止违法使用和商用,产生法律纠纷本站概不负责!如果侵犯了您的权益请与我们联系!
转载请注明出处: 免费源码网-免费的源码资源网站 » Flutter中的Key
发表评论 取消回复