key参数基本上在每个部件构造函数中都能找到,但其使用并不普遍。当部件在部件树中移动时,key会保持状态。在实践中,这意味着它们可以用来保存用户的滚动位置或在修改集合时保持状态。
【参考文献】
文章:[Keys! What are they good for?](medium.com/flutter/key…
作者:Emily Fortuna
上述译文仅供参考,具体内容请查看上面链接,解释权归原作者所有。
以下文章改编自以下视频:youtu.be/kn0EOS-ZiIc
(PS:如果您更喜欢听/看而不是阅读,该视频应涵盖所有相同的内容。)
关于 Key 的内幕
大多数情况下......你并不需要keys!一般来说,添加key没有坏处,但也没有必要,只会占用不必要的空间,就像 new 关键字或在新变量的右侧和左侧声明类型一样(我在看着你,Map<Foo, Bar> aMap = Map<Foo,Bar>())。但是,如果你发现自己需要添加、移除或重新排序持有某些状态的同类型Widget,那么使用key很可能是你的未来!
为了说明为什么在修改Widget时需要key,我编写了一个非常简单的应用程序,其中有两个随机颜色的部件,当你点击按钮时,这两个部件会交换位置:
在应用程序的stateless版本中,我在一行中放置了两个stateless tile,每个tile的颜色都是随机生成的,在Row中和一个名为 PositionedTiles 的 StatefulWidget 来存储这些图块的位置。当我点击底部的 FloatingActionButton 时,它们在列表中的位置就会正确对调:
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)));
}
}
但是,如果我们将这些 ColorfulTiles 变为Stateful而非Stateless,并在状态中存储颜色,那么当我按下按钮时,看起来就像什么也没发生一样。
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 Widget 添加一个关键参数,然后部件就会按照我们的要求交换位置:
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),
));
}
}
但是,只有在修改的子树中有Stateful的widget时,才需要这样做。如果集合中的整个部件子树(Widget tree)都是stateless的,就不需要key了。
就是这样!从技术上讲,这就是在 Flutter 中使用key所需了解的全部内容。但如果您想了解其中的根本原因,....
为什么有时需要Keys的细节
正如您所知道的,Flutter 会为每个widget创建一个相应的元素。就像构建部件树(Widget tree)一样,Flutter 也会构建元素树(ElementTree)。元素树(ElementTree)非常简单,只保存每个widget的类型信息和对子元素的引用。您可以将元素树视为 Flutter 应用程序的骨架。它显示了应用程序的结构,但所有附加信息都可以通过引用原始部件来查找。
Flutter 构建了相应的 Element。就像构建 Widget tree 一样,Flutter 也构建了 Element tree。 ElementTree非常简单,仅保存有关每个widgetwidget类型的信息以及对子元素的引用。您可以将 ElementTree 视为 Flutter 应用程序的骨架。它显示了应用程序的结构,但可以通过引用原始widget来查找所有附加信息。
上例中的 Row widget实质上为其每个子部件保存了一组有序插槽。当我们调换 Row 中 Tile widget 的顺序时,Flutter 会遍历 ElementTree(元素树),查看骨架结构是否相同。
它从RowElement 开始,然后移动到其子节点。ElementTree 会检查新widget是否与旧widget的类型和key值相同,如果相同,它就会更新对新widget的引用。在stateless版本中,widget没有key,所以 Flutter 只检查类型。(如果这些信息看起来太多,请观看上面的动画图)。
Stateful Widget的底层 Element tree结构看起来有些不同。和之前一样,这里有widget和元素,但也有一对状态对象,颜色信息存储在这里,而不是widget本身。
在没有key的Stateful Tile 情况下,当我交换两个widget的顺序时,Flutter 会遍历 ElementTree,检查 RowWidget 的类型,并更新引用。然后 TileElement 检查相应的widget是否是同一类型(TileWidget),并且确实如此,因此它更新了引用。同样的情况也发生在第二个子元素身上。因为 Flutter 使用 ElementTree 及其相应的状态来确定在您的设备上实际显示的内容,从我们的角度来看,您的widget似乎没有正确交换!
在具有Stateful Tiles 的固定版本中,我向widget添加了关键属性。如果我们交换widget,the Row widget会像以前一样匹配,但 Tile Element 的key与相应 Tile Widget 的key不匹配。这会导致 Flutter 停用这些元素并删除对元素树中 Tile 元素的引用,从第一个不匹配的元素开始。
然后,Flutter 会在Row非匹配子代中寻找具有正确对应key的元素。找到匹配后,它就会更新对相应 widget 的引用。然后,Flutter 对第二个子元素做同样的操作。现在,当我按下按钮时,Flutter 将显示我们所期望的效果,即widget交换位置并更新颜色。
因此,总而言之,如果您要修改一个集合中的Stateful Widget的顺序或数量,key是非常有用的。为了便于说明,我在本例中将颜色存储为状态。不过,状态通常更为微妙。播放动画、显示用户输入的数据以及滚动位置都涉及状态。
我应该把它们放在哪里?
简短回答:如果需要在应用程序中添加key,应将其添加到需要保留状态的 widget 子树的顶端。
我见过的一个常见错误是,人们认为只需在第一个Stateful的 widget 上添加key,但这是不可能的。不信?为了说明我们会遇到什么样的麻烦,我将 colorfulTile widget 与 padding widget 封装在一起,但将key留在了瓷砖上。
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),
));
}
}
现在,当我点击按钮时,瓷砖会变成完全不同的随机颜色!
下面是添加了填充部件后的 WidgetTree 和 ElementTree 的外观:
当我们交换子元素的位置时,Flutter 的元素到小部件匹配算法会一次查看树中的一个层级。在图中,我们将子元素的子元素涂成灰色,这样我们就可以一次只关注一层。在带有 Padding 元素的第一层子元素中,一切都匹配正确。
在第二层,Flutter 会注意到磁贴元素的key与部件的key不匹配,因此会停用该磁贴元素,放弃这些连接。我们在本例中使用的key是本地key。这意味着在匹配 widget 和元素时,Flutter 只查找树中特定层级内的key匹配。
由于在该层级中找不到具有该key值的磁贴元素,因此它会创建一个新的元素,并初始化一个新的状态,在本例中就是将 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 API 提供商为我们提供了各种密钥类供我们选择。您应该使用哪种类型的密钥取决于需要密钥的项目的显著特征是什么。请看一下您存储在这些部件中的信息。下面我将介绍四种不同类型的key: 值key(ValueKey)、对象key(ObjectKey)、唯一key(UniqueKey)和唯一key(UniqueKey)。
考虑一下下面的待办事项列表应用程序¹,在这个应用程序中,你可以根据优先级对待办事项列表中的项目重新排序,然后在完成后删除它们。
在这种情况下,您可能希望待办事项的文本是恒定和唯一的。如果是这种情况,那么它可能是 ValueKey 的理想候选项,其中文本就是 "值"。
return TodoItem(
key: ValueKey(todo.task),
todo: todo,
onDismissed: (direction) => _removeTodo(context, todo),
);
在另一种情况下,也许您有一个列出每个用户信息的地址簿应用程序。在这种情况下,每个子部件存储的数据组合更为复杂。任何单个字段(如名字或生日)都可能与另一个条目相同,但组合是唯一的。在这种情况下,ObjectKey 可能是最合适的。
如果您的集合中有多个具有相同值的部件,或者如果您想真正确保每个部件都与其他部件不同,则可以使用 **UniqueKey**。在颜色切换应用的示例中,我使用了 UniqueKey,因为我们在磁贴中没有存储任何其他常量数据,而且我们也不知道在构建部件时颜色会是什么。使用 UniqueKey 时要小心!如果你在构建方法中build了一个 new UniqueKey,那么每次重新执行构建方法时,使用该key的 widget 都会得到一个不同的唯一key。这将消除使用key的任何好处!
同样,你绝对不希望使用随机数作为密钥。每次构建部件时,都会生成一个新的随机数,这样就会失去帧与帧之间的一致性。那还不如一开始就不使用密钥!
页面存储key(**PageStorageKeys**)是一种专门的key,用于存储用户的滚动位置,以便应用程序日后保存。
全局key(**GlobalKey**)有两种用途:它们允许窗口小部件在不丢失状态的情况下在应用程序中的任意位置更换父节点,或者用于访问窗口小部件树中完全不同部分的另一个窗口小部件的信息。第一种情况的例子是,如果您想在两个不同的屏幕上显示相同的 widget,但保持相同的状态,您就需要使用 GlobalKey。在第二种情况下,也许你想验证密码,但不想与树中的其他部件共享状态信息。GlobalKey还可以用于测试,使用key可以访问特定的部件并查询其状态信息。
通常(但并不总是!),GlobalKeys 有点像全局变量。通常有更好的方法来查询该状态,如使用 InheritedWidgets 或类似 Redux 或 BLoC 模式。
快速总结
总之,当你想在部件树中保留状态时,可以使用key值。最常见的情况是当你修改相同类型的部件集合时,比如在列表中。将key放在要保留状态的 widget 树的顶端,并根据 widget 中要存储的数据来选择key的类型。
///end///
⚠️:文章翻译上如有语法不准确或者内容纰漏,欢迎各位评论区指正。
【关于TalkX】
TalkX是一款基于GPT实现的IDE智能开发插件,专注于编程领域,是开发者在日常编码中提高编码效率及质量的辅助工具,TalkX常用的功能包括但不限于:解释代码、中英翻译、性能检查、安全检查、样式检查、优化并改进、提高可读性、清理代码、生成测试用例等。
TalkX建立了全球加速网络,不需要考虑网络环境,响应速度快,界面效果和交互体验更流畅。并为用户提供了OpenAI的密钥,不需要ApiKey,不需要自备账号,不需要魔法。
TalkX产品支持:JetBrains (包括 IntelliJ IDEA、PyCharm、WebStorm、Android Studio)、HBuilder、VS Code、Goland.