key之间的关系
key
参考:https://api.flutter.dev/flutter/widgets/Widget/key.html
控制一个widget该如何替换掉树中的另一个widget:如果两个widget的runtimeType 和 key都相等(==),那么会让旧widget所对应的旧element转而指向新widget(即调用element.update);否则,旧element会被从树中移除,然后根据新widget来生成一个新element,并将该新element插入到树中。
@immutable
abstract class Key {
const factory Key(String value) = ValueKey<String>;
@protected
const Key.empty();
}
默认创建 Key 将会通过工厂方法根据传入的 value 创建一个 ValueKey。
Key 派生出两种不同用途的 Key:LocalKey 和 GlobalKey。
Key的用途
参考:https://api.flutter.dev/flutter/foundation/Key-class.html
大多数时候并不需要使用key。
当需要在一个StatefulWidget集合中进行添加、删除、重排序等操作时,才可能需要使用到 key。
下面的例子来自 Flutter 的官方视频: 何时使用密钥 - Flutter小部件 101 第四集
为了说明在修改 widget 集合的时候为什么需要 key,我编写了一个非常简单的应用程序,其中有两个随机背景色的 widget,点击按钮时它们会交换位置:
![Flutter 中的key、LocalKey、GlobalKey_ico](//dev-img.mos.moduyun.com/20231025/9437f112-ab4f-44bc-9b7d-2959aa4bdb61.gif)
在无状态的版本里,一个Row
中放置两个带有随机背景色的无状态 (stateless) 的StatelessColorfulTile
,使用继承自StatefulWidget
的PositionedTiles
存储这些 tile 的位置。当点击底部的FloatingActionButton
时,可以正确地交换 tile 在列表中的位置:
void main() => runApp(new MaterialApp(home: PositionedTiles()));
class PositionedTiles extends StatefulWidget {
@override
State<StatefulWidget> createState() => PositionedTilesState();
}
class PositionedTilesState extends State<PositionedTiles> {
List<Widget> tiles = [
StatelessColorfulTile(),
StatelessColorfulTile(),
];
@override
Widget build(BuildContext context) {
return Scaffold(
body: Row(children: tiles),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.sentiment_very_satisfied), onPressed: swapTiles),
);
}
swapTiles() {
setState(() {
tiles.insert(1, tiles.removeAt(0));
});
}
}
class StatelessColorfulTile extends StatelessWidget {
Color myColor = UniqueColorGenerator.getColor();
@override
Widget build(BuildContext context) {
return Container(
color: myColor, child: Padding(padding: EdgeInsets.all(70.0)));
}
}
但是,当把 tile 替换成有状态 (stateful) 的StatefulColorfulTile
并将颜色存储在State
中时,点击按钮,界面看上去并没有任何变化。
![Flutter 中的key、LocalKey、GlobalKey_flutter_02](//dev-img.mos.moduyun.com/20231025/6a227616-44d7-4c2f-841f-1b88ee76fb81.gif)
List<Widget> tiles = [
StatefulColorfulTile(),
StatefulColorfulTile(),
];
...
class StatefulColorfulTile extends StatefulWidget {
@override
ColorfulTileState createState() => ColorfulTileState();
}
class ColorfulTileState extends State<ColorfulTile> {
Color myColor;
@override
void initState() {
super.initState();
myColor = UniqueColorGenerator.getColor();
}
@override
Widget build(BuildContext context) {
return Container(
color: myColor,
child: Padding(
padding: EdgeInsets.all(70.0),
));
}
}
提醒一下,上面显示的代码是有问题的。因为当用户按下“交换”按钮时,色块并没有交换。解决方法是向有状态 (stateful) 的 widgets 添加一个 key 参数。然后,widgets 开始按我们的预期正确交换位置:
![Flutter 中的key、LocalKey、GlobalKey_flutter_03](//dev-img.mos.moduyun.com/20231025/fcc6caaf-98de-4b9f-b413-b03860d124eb.gif)
List<Widget> tiles = [
StatefulColorfulTile(key: UniqueKey()), // Keys added here
StatefulColorfulTile(key: UniqueKey()),
];
...
class StatefulColorfulTile extends StatefulWidget {
StatefulColorfulTile({Key key}) : super(key: key); // NEW CONSTRUCTOR
@override
ColorfulTileState createState() => ColorfulTileState();
}
class ColorfulTileState extends State<ColorfulTile> {
Color myColor;
@override
void initState() {
super.initState();
myColor = UniqueColorGenerator.getColor();
}
@override
Widget build(BuildContext context) {
return Container(
color: myColor,
child: Padding(
padding: EdgeInsets.all(70.0),
));
}
}
解决办法2,覆写State的didUpdateWidget方法,当widget的配置发生变化时,手动更新state,例如:
///Called whenever the widget configuration changes.
void didUpdateWidget(TimePickerBar oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.initValue != widget.initValue) {
_currentTime = widget.initValue;
_tempTime = DateTime.fromMillisecondsSinceEpoch(
_currentTime.millisecondsSinceEpoch);
}
}
当子树 (subtree) 中存在有状态 (stateful) 的 widget 时,修改子树时才需要 key 来维护 widget 状态。如果整个 widget subtree 中都是无状态 (stateless) 的 widgets,key 是不需要的。
从技术上讲,这就是在 Flutter 中使用 key 所需要知道的全部内容。但是,如果您想了解这一切的根本原因……
为什么有时候 Key 是需要的
众所周知,Flutter 为每个 widget 构建一个对应的 Element
。就像构建 Widget 树一样,Flutter 也同时构建了 Element
树。ElementTree
很简单,仅仅保存 widget 的类型信息和对子元素的引用。可以认为 ElementTree
是 Flutter 应用程序的骨架。它显示了应用程序的结构,所有的其他附加信息都可以通过引用原始的 widget 来查找到。
上面示例中的 Row
widget 实际上为它的每个子 widget 保存了一组有序的插槽 (slot)。当交换 Row
中 Tile widgets 的顺序时,Flutter将遍历ElementTree
,以查看骨架结构是否与之相同。
![Flutter 中的key、LocalKey、GlobalKey_ico_04](//dev-img.mos.moduyun.com/20231025/d155eae3-4e93-4a3d-ab8f-7121ac94d234.gif)
从 RowElement
开始,依次遍历子元素。RowElement
检查新 widget 的 类型 (type) 和 key 是否和持有的老 widget 相同,如果相同,则将引用指向新的 widget。在无状态 (stateless) 版本中,widget 并没有 key,所以 Flutter 仅仅检查类型 (type) 是否相同。(如果这看起来信息量太多,请仔细参考上面的动图)。
有状态 (stateful) widget 对应的 Element
tree 的结构看起来有些不同。widgets 和 elements 都与无状态版本的时候一样,但多了一个相关联的状态对象 (state object)。有状态 (stateful) widget 的颜色信息是存储在 State
对象中,而不是存储在 widget 本身中。
![Flutter 中的key、LocalKey、GlobalKey_ico_05](//dev-img.mos.moduyun.com/20231025/fa5ce55d-7fe1-45c7-86d7-1c86b06db1e9.gif)
当使用有状态的Tile
,且没有传入 key 的情况下,交换两个 widget 的顺序时,Flutter 会查看ElementTree
,检查RowWidget
的类型并更新引用。随后TileElement
会检查相应 widget 的类型 (type) 是否与之相同,并更新对 widget 引用。这里 widget 的类型显然是相同的。同样的事情在第二个 child 上也会做一遍。由于 Flutter 使用ElementTree
和 与之对应的state对象来决定什么内容应该显示在你的设备上,从我们人类的视角看,weidget 并没有正确的进行交换。
![Flutter 中的key、LocalKey、GlobalKey_ico_06](//dev-img.mos.moduyun.com/20231025/fcd5e176-4790-4644-a630-7fd7f0e8888f.gif)
使用有状态Tile
的修复了该问题版本中,我们将 key 添加到了 widget 中。现在再交换 widget,Row
widgets 像之前版本一样得到匹配,但 Tile Element 的 key 和对应的 Tile Widget 的 key 并不匹配。这将导致 Flutter 从第一个不匹配的元素开始,停用 (deactivate) 这些不匹配的 elements,并将这些 Tile Element 的引用从 Element Tree 中移除。
![Flutter 中的key、LocalKey、GlobalKey_flutter_07](//dev-img.mos.moduyun.com/20231025/b653d263-2a86-425b-95ca-b5945db348b4.gif)
然后,Flutter 会在 Row
的不匹配的 child elements 中查找具有正确 key 的 element。找到匹配项后,更新 element 对 widget 的引用。然后,对第二个孩子做同样的事情。现在 Flutter 将按照我们所期望的显示,按下按钮时,widget 会交换位置并更新它们的颜色。
总而言之,当修改集合中有状态 widget 的顺序或数量时,key 很有用。为了便于说明,此示例中将颜色存储为状态。但是,状态往往比这要隐晦得多。播放动画、显示用户输入的数据、滚动位置都涉及到状态。
Key 应该放在哪里?
简单的说:如果需要给 app 添加 key,应该把 key 添加到需要维护状态的 widget subtree 的顶部。
我见过的一个常见错误是:人们认为他们只需要在第一个有状态的 widget 上放一个 key,但“此处危险”。不相信我?为了展示会遇到什么样的麻烦,我用 Padding
widget 包裹 colourfulTile widget,但把 key 留在了 tiles 上。
void main() => runApp(new MaterialApp(home: PositionedTiles()));
class PositionedTiles extends StatefulWidget {
@override
State<StatefulWidget> createState() => PositionedTilesState();
}
class PositionedTilesState extends State<PositionedTiles> {
// Stateful tiles now wrapped in padding (a stateless widget) to increase height
// of widget tree and show why keys are needed at the Padding level.
List<Widget> tiles = [
Padding(
padding: const EdgeInsets.all(8.0),
child: StatefulColorfulTile(key: UniqueKey()),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: StatefulColorfulTile(key: UniqueKey()),
),
];
@override
Widget build(BuildContext context) {
return Scaffold(
body: Row(children: tiles),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.sentiment_very_satisfied), onPressed: swapTiles),
);
}
swapTiles() {
setState(() {
tiles.insert(1, tiles.removeAt(0));
});
}
}
class StatefulColorfulTile extends StatefulWidget {
StatefulColorfulTile({Key key}) : super(key: key);
@override
ColorfulTileState createState() => ColorfulTileState();
}
class ColorfulTileState extends State<ColorfulTile> {
Color myColor;
@override
void initState() {
super.initState();
myColor = UniqueColorGenerator.getColor();
}
@override
Widget build(BuildContext context) {
return Container(
color: myColor,
child: Padding(
padding: EdgeInsets.all(70.0),
));
}
}
此时,单击按钮,Tiles 会变成完全不同的随机颜色!
![Flutter 中的key、LocalKey、GlobalKey_ico_08](//dev-img.mos.moduyun.com/20231025/53747caf-38cd-4ae9-9fe4-1397f65ac49c.gif)
下图展示了包裹Padding
widget 后,WidgetTree 和 ElementTree 的情况:
![Flutter 中的key、LocalKey、GlobalKey_ide_09](//dev-img.mos.moduyun.com/20231025/15b20f90-af29-4a7b-9e31-1199e168ec12.jpg)
当交换子节点的位置时,Flutter 的 element-to-widget 匹配逻辑一次只会检查树的一个层次。下图中将“孙子节点”(子节点的子节点)置灰,方便我们一次只专注于一个层次。Padding
elements 所在的第一层,一切都正确匹配。
![Flutter 中的key、LocalKey、GlobalKey_ico_10](//dev-img.mos.moduyun.com/20231025/ee309483-a6b4-4f56-bffe-7819fb6b6d84.gif)
在第二层,Flutter 发现 Tile Element 的 key 和 Tile Widget 的 key 不一致,deactivate 了 Tile Element,并丢弃了这些连接。这个例子中使用的是 LocalKeys,这意味着在将 widget 和 element 做匹配时,Flutter 只在树的特定层级中寻找 key 的匹配关系。
由于在这一层找不到具有该 key 值的 tile element,所以创建了一个新的 Element,并初始化为一个新状态,在本例中,小部件变为了橙色!
![Flutter 中的key、LocalKey、GlobalKey_ico_11](//dev-img.mos.moduyun.com/20231025/e224cbba-4784-4f3f-b94d-a47341a867ad.gif)
如果在 padding widget 层级添加 key:
void main() => runApp(new MaterialApp(home: PositionedTiles()));
class PositionedTiles extends StatefulWidget {
@override
State<StatefulWidget> createState() => PositionedTilesState();
}
class PositionedTilesState extends State<PositionedTiles> {
List<Widget> tiles = [
Padding(
// Place the keys at the *top* of the tree of the items in the collection.
key: UniqueKey(),
padding: const EdgeInsets.all(8.0),
child: StatefulColorfulTile(),
),
Padding(
key: UniqueKey(),
padding: const EdgeInsets.all(8.0),
child: StatefulColorfulTile(),
),
];
@override
Widget build(BuildContext context) {
return Scaffold(
body: Row(children: tiles),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.sentiment_very_satisfied), onPressed: swapTiles),
);
}
swapTiles() {
setState(() {
tiles.insert(1, tiles.removeAt(0));
});
}
}
class StatefulColorfulTile extends StatefulWidget {
StatefulColorfulTile({Key key}) : super(key: key);
@override
ColorfulTileState createState() => ColorfulTileState();
}
class ColorfulTileState extends State<ColorfulTile> {
Color myColor;
@override
void initState() {
super.initState();
myColor = UniqueColorGenerator.getColor();
}
@override
Widget build(BuildContext context) {
return Container(
color: myColor,
child: Padding(
padding: EdgeInsets.all(70.0),
));
}
}
Flutter 正确地更新连接,就像我们在前面的示例中所做的那样。宇宙又恢复了秩序。
![Flutter 中的key、LocalKey、GlobalKey_ide_12](//dev-img.mos.moduyun.com/20231025/4bc46987-4419-425e-8c65-a96db2f9dfe1.gif)
以上关于卡片交互的举例介绍翻译自外网,原文:https://medium.com/flutter/keys-what-are-they-good-for-13cb51742e7d
中文翻译:
Flutter: Key! 它们有什么用处 - 知乎
LocalKey
LocalKey 直接继承至 Key,它应用于拥有相同父 Element 的小部件进行比较的情况,也就是上述例子中,有一个多子 Widget 中需要对它的子 widget 进行移动处理,这时候你应该使用Localkey。
Localkey 派生出了许多子类 key:
- ValueKey : ValueKey(‘String’)
- ObjectKey : ObjectKey(Object)
- UniqueKey : UniqueKey()
- Valuekey 又派生出了 PageStorageKey : PageStorageKey(‘value’)
GlobalKey
@optionalTypeArgs
abstract class GlobalKey<T extends State<StatefulWidget>> extends Key {
···
static final Map<GlobalKey, Element> _registry = <GlobalKey, Element>{};
static final Set<Element> _debugIllFatedElements = HashSet<Element>();
static final Map<GlobalKey, Element> _debugReservations = <GlobalKey, Element>{};
···
BuildContext get currentContext ···
Widget get currentWidget ···
T get currentState ···
GlobalKey 使用了一个静态常量 Map 来保存它对应的 Element。
你可以通过 GlobalKey 找到持有该GlobalKey的 Widget,State 和 Element。
注意:GlobalKey 是非常昂贵的,需要谨慎使用。
什么时候需要使用 Key
ValueKey
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;
}
}
源码也解释的比较清楚了 ValueKey继承LocalKey,可以接受任何类型的参数(T),但同一个ValueKey不能出现两次甚至多次,所以要使用Operator函数去鉴别ValueKey是否唯一,也就是ValueKey的值相等。
请思考下面的 To-do list app1 ,它可以根据优先级重新排列 TODO 列表中的条目,并且在完成后将 TODO 项删除。
![Flutter 中的key、LocalKey、GlobalKey_ide_13](//dev-img.mos.moduyun.com/20231025/730aaf1c-2df3-4207-8e4d-8ef0a8a06699.gif)
在这个场景中,假设 TODO 项的文本是不变的、且唯一的,那么,这是一个很好的使用ValueKey
的场景。其中文本是ValueKey
的“值”。
return TodoItem(
key: ValueKey(todo.task),
todo: todo,
onDismissed: (direction){
_removeTodo(context, todo);
},
);
ObjectKey
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);
}
}
ObjectKey和ValueKey的区别再于Identical 和 == 。Identical是判断是否为同一内存(内存中的指针是否相同),类似于java中的 == 判断。
当然Key这个东西简便好,使用ObjectKey的情况下也比较少。当然如果Key比较复杂的话使用ObjectKey也是个不错的选择如果你有一个生日应用,它可以记录某个人的生日,并用列表显示出来,同样的还是需要有一个滑动删除操作。
我们知道人名可能会重复,这时候你无法保证给 Key 的值每次都会不同。但是,当人名和生日组合起来的 Object 将具有唯一性。
这时候你需要使用 ObjectKey!
UniqueKey
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)}]';
}
源码以看啥也没有 就只剩下一些翻译,那下面我们再来看看这个独一无二的Key怎么理解把。
丢状态
AnimatedSwitcher(
duration: const Duration(seconds: 1),
child: Text("no keyrrths", key: UniqueKey()),
)
每次改变文字时,假如不传uniqueKey,就不会有动画的渐变效果,而如果传了UniqueKey,则会有渐变动画效果。因为不传uniqueKey时,每次都只会认为text的widget发生了变化,只会将text的widget给替换为新的widget,而element还是同一个不会变化,所以会认为UI没有发生变化,因此不会改变;而如果传了uniqueKey时,每次widget比较时都会因为自身的key不一致而被认为是不同的widget,导致会重建element和renderObject,前后两个UI不一致,此时就会发生动画效果。
如果组合的 Object 都无法满足唯一性的时候,你想要确保每一个 Key 都具有唯一性。那么,你可以使用 UniqueKey。它将会通过该对象生成一个具有唯一性的 hash 码。
不过这样做,每次 Widget 被构建时都会去重新生成一个新的 UniqueKey,失去了一致性。也就是说你的小部件还是会改变。
PageStorageKey
当你有一个滑动列表,你通过某一个 Item 跳转到了一个新的页面,当你返回之前的列表页面时,你发现滑动的距离回到了顶部。这时候,给 Sliver 一个 PageStorageKey!它将能够保持 Sliver 的滚动状态。
![Flutter 中的key、LocalKey、GlobalKey_ide_14](//dev-img.mos.moduyun.com/20231025/97531c4c-3167-4503-9ebf-82603d07cb89.gif)
GlobalKey
GlobalKey 能够跨 Widget 访问状态。 在这里我们有一个 Switcher 小部件,它可以通过 changeState 改变它的状态。
class SwitcherScreenState extends State<SwitcherScreen> {
bool isActive = false;
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Switch.adaptive(
value: isActive,
onChanged: (bool currentStatus) {
isActive = currentStatus;
setState(() {});
}),
),
);
}
changeState() {
isActive = !isActive;
setState(() {});
}
}
但是我们想要在外部改变该状态,这时候就需要使用 GlobalKey。
class _ScreenState extends State<Screen> {
final GlobalKey<SwitcherScreenState> key = GlobalKey<SwitcherScreenState>();
@override
Widget build(BuildContext context) {
return Scaffold(
body: SwitcherScreen(
key: key,
),
floatingActionButton: FloatingActionButton(onPressed: () {
key.currentState.changeState();
}),
);
}
}
这里我们通过定义了一个 GlobalKey 并传递给 SwitcherScreen。然后我们便可以通过这个 key 拿到它所绑定的 SwitcherState 并在外部调用 changeState 改变状态了。
通常(但并非总是!),GlobalKey
有点像全局变量。Flutter 中有一种更好的方式来查看状态,即使用 InheritedWidget
, 或 Redux、BLoC 模式之类的东西。
关于GlobalKey典型使用参考:flutter通过GlobalKey在自定义Widget外部获取其state刷新页面_一叶飘舟的博客
上面是通过GlobalKey获取state,下面我们看一下从GlobalKey获取element。
GlobalKey用于Form表单
登录肯定要有输入用户名和密码的输入框,在Flutter
中我们只用Form
表单+TextFormField
的形式加以实现。现在就来讲讲Form
和TextFormField
的简单使用,demo中登录界面如下:
![在这里插入图片描述 Flutter 中的key、LocalKey、GlobalKey_ico_15](//dev-img.mos.moduyun.com/20231025/8d6161a3-a9e5-4249-bf24-5d4a1d0cf36c.png)
然后我们在不输入任何字符的情况下点击submit按钮,效果如下所示:
![在这里插入图片描述 Flutter 中的key、LocalKey、GlobalKey_ide_16](//dev-img.mos.moduyun.com/20231025/9d1eab97-0f42-4a56-be02-74ea85e0f36a.png)
上图布局的代码如下所示:
![在这里插入图片描述 Flutter 中的key、LocalKey、GlobalKey_flutter_17](//dev-img.mos.moduyun.com/20231025/6516f6e2-e6e3-427d-b3ff-fc5b221d6070.png)
如上所示首先初始化GlobalKey
对象。然后将此对象设置为Form
的key,最后再点击Submit按钮的时候,我们没有直接操作TextFormField
,而是通过_formKey.currentState.validate
对输入框TextFormField
的内容进行非空验证。代码中的_formKey.currentState
其类型是FormState
:
class Form extends StatefulWidget {
const Form({
Key key,
@required this.child,
}) ;
@override
FormState createState() => FormState();
//调用Form.of(context)也可以获取FormState对象
//详情请看【Flutter之实战InheritedWidget详解】
static FormState of(BuildContext context) {
final _FormScope scope = context.inheritFromWidgetOfExactType(_FormScope);
return scope?._formState;
}
}
GlobalKey获取Element的原理
abstract class GlobalKey<T extends State<StatefulWidget>> extends Key {
//一个静态的变量map集合
static final Map<GlobalKey, Element> _registry = <GlobalKey, Element>{};
}
,从 GlobalKey<T extends State<StatefulWidget>>
的类结构可以看出,GlobalKey
主要用来存储状态信息 State<StatefulWidget>
,State
指的是StatefulWidget
的状态类,通过StatefulWidget
的createState
方法创建:
abstract class StatefulWidget extends Widget {
//Key是个options的,可以设置也可以不设置
const StatefulWidget({ Key key }) : super(key: key);
@protected
State createState();
}
上文中为什么通过GlobalKey.currentState
就可以获取到FormState
呢?二者是怎么关联起来的呢?现在就来一探究竟。
先来看看GlobalKey
的currentState
方法的具体实现:
T get currentState {
//当前的Element对象
final Element element = _currentElement;
//检测是否是SatefulElement对象
if (element is StatefulElement) {
final StatefulElement statefulElement = element;
//获取StatefulElement对象的State对象
final State state = statefulElement.state;
//如果状态匹配,则返回对应的T
if (state is T)
return state;
}
return null;
}
//_currentElement是一个map集合Map<GlobalKey, Element>
//该集合以GlobalKeyweight对象,其值保存的是Element。
Element get _currentElement => _registry[this];
在GlobalKey
内部有一个静态的的_registry Map
集合,该集合以GlobalKey
为key,以Element
为value;其提供的currentState
方法就是以GlobalKey
对象为Key
获取对应的StatefulElement
对象,然后从StatefulElement.state
里获取具体的值FormState
,那么什么时候往_registry 集合里填充数据呢?通过Fultter之Element和Widget对应关系解析我们知道一个Element
在创建之后会调用mount
方法:
void mount(Element parent, dynamic newSlot) {
///省略部分代码
if (widget.key is GlobalKey) {
final GlobalKey key = widget.key;
//将Element对象注册进来
key._register(this);
}
}
//GlobalKey的_register方法。
void _register(Element element) {
_registry[this] = element;
}
可以发现在mount方法将我们创建的Element
注入到GlobalKey
的静态map集合中去!所以GlobalKey
的作用就是:*持有当前Widget
的Element
对象,因此通过GlobalKey
对象可以获取到当前StatefulWidget
的StatefullElement
,在通过StatefullElement
获取State
状态对象,从而操控State
的相关方法。比如FormState
的validate()方法进行非空校验。
事实上我们还可以使用Form.of(context)
方法也可以获到FormState
对象,然后调用validate方法完成TextFormField
的非空校验,其中原理,详细解析见Flutter之实战InheritedWidget详解
GlobalKey的方法
我们可以通过GlobalKey来找到对应的widget,甚至是state等更多相关的东西.
![image.png Flutter 中的key、LocalKey、GlobalKey_ico_18](//dev-img.mos.moduyun.com/20231025/6da80b93-1513-4228-8161-7b62c7298eb1.png)
可以看到,global对象提供了几个方法,我们主要用到的就是前三个,可以通过globalKey来找到对应的BuildContext、State以及Widget.
获取对应的state
比如我们可以通过这个globalKey来找到对应的按钮中的数字,我们打印一下看一看:
![asda.gif Flutter 中的key、LocalKey、GlobalKey_ide_19](//dev-img.mos.moduyun.com/20231025/f17e4685-2157-48e7-820a-b192d7600a7e.gif)
对应的代码:
floatingActionButton: FloatingActionButton(
onPressed: () {
print((_globalKey.currentState as _BoxState).count);
},
child: Icon(Icons.wifi_protected_setup),
)
可以看到,上面我进行了一个类型转换,因为globalKey是任何widget都可以使用的,flutter在运行之前并不知道具体是哪个widget用到了它,所以我们这要转换成对应的类型.
那如果我们想改变里面的值呢,是否可以做到呢?
我们改变一下onPress的代码:
onPressed: () {
final state = _globalKey.currentState as _BoxState;
state.count++;
print(state.count);
state.setState(() {});
}
切记,一定要进行setState操作,来触发Ui的刷新,否则值会修改,但是没有体现在UI上.
更好的写法是
state.setState(() {
state.count++;
});
推荐把setState需要刷新的对象放入这个大括号{}内,因为在实际开发中,并不是每次有对象变化的时候多需要刷新UI的,如果能够按照规范将需要刷新的对象放到setState中,等到需求变更,代码需要修改的时候,setState中是空的时候,我们就知道不需要刷新UI了,就可以删除掉这个setState.
反之,如果是写到外面,当我们把需要刷新的对象的代码删除掉的时候,不知道这个setState到底还有没有用(毕竟实际我们可能是多人开发,也可能自己以前写的代码不记得了),就导致了额外的刷新操作.
当然,其实上述操作也不是推荐的做法,一般都是由自己内部来管理它的状态,如果有多个widget共用的话,则应该进行变量提示操作,把变量写到外层.
获取对应的widget
刚才我们也看到了,除了currentState,它还有一些别的东西.
比如currentWidget,当然我们代码对应的类型也要做改变了.
onPressed: () {
final widget = _globalKey.currentWidget as Box;
print(widget.color);
}
比如这里,因为widget是Box,所以类型转换为Box. 通过currentWidget,我们就可以得到对应的widget中的属性,例如此处的color.
获取对应的context
通过currentContext,可以获得对应的context,context其实指的就是element.
在第二章中我们介绍到了element tree,并没有详细说element.Element的作用还是比较多的.
比如我想知道对应组件的尺寸,位置.这其实都是难以获得的信息.
final renderBox = _globalKey.currentContext!.findRenderObject() as RenderBox;
此处,我们通过currentContext!.findRenderObject找到了对应的renderObject,RenderObject的类型还是比较多的.因为我们此处的Box刚好是RenderBox, 所以将类型转换为RenderBox.
![image.png Flutter 中的key、LocalKey、GlobalKey_flutter_20](//dev-img.mos.moduyun.com/20231025/05c0c396-1456-485c-8270-4c10c7b66045.png)
可以看到renderBox有一个对应的size方法,通过它就可以拿到对应widget的尺寸.
如果想拿到它的位置信息的话,可以通过
renderBox.localToGlobal(Offset.zero);
来拿到对应widget的左上角距离屏幕左上角的坐标信息.
看一下分别打印的信息:
I/flutter ( 2158): Size(100.0, 100.0) //对应widget的尺寸
I/flutter ( 2158): Offset(100.0, 80.0) //对应widget的坐标
小结
最后做一个简单的总结
本章主要介绍了globalKey的两种用法,两种用法都是先声明一个globalKey,再把这个globalKey传给想要放到的widget里面.
第一种用法,保证有了globalKey的widget可以随便改变它在Wdiget Tree中的位置,而它的状态不会丢失.
第二种用法,相当于通过这个key将widget暴露了出来,借助它就可以利用类似于getElementById之类的方式去找到它各种各样的东西:
- currentContext: 可以找到包括renderBox在内的各种element有关的东西
- currentWidget: 可以得到widget的属性
- currentState: 可以得到state里面的变量