扑 钥匙! 他们是干什么的?


几乎在每个小部件构造函数中都可以找到key参数,但是在开发中很少使用此参数。 在小部件树中移动小部件时, Keys保留状态。 实际上,这意味着它们对于保存用户的滚动位置或在集合更改时保存状态很有用。


本文改编自以下视频 。 如果您喜欢听/看而不是阅读,那么视频将为您提供相同的材料。


有关keys秘密信息


大多数时候...不需要keys 。 通常,添加它们没有害处,但这也是没有必要的,因为它们只是作为新关键字或类型声明出现在新变量的两侧(我在谈论您, Map<Foo, Bar> aMap = Map<Foo, Bar>() )。


但是,如果您发现要在集合中添加,删除或重新排列包含状态且类型相同的小部件,则应注意keys

为了演示为什么更改小部件集合时需要keys ,我编写了一个非常简单的应用程序,其中包含两个彩色小部件,这些小部件在单击按钮时会更改位置:



在此版本的应用程序中,我在Row和PositionedTiles小部件中分别具有两个随机颜色的无状态小部件( StatelessWidget ),并在StatelessWidget中放置了状态( 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))); } } 

但是,如果我们将状态添加到颜色小部件中(使它们成为StatefulWidget )并在其中存储颜色,那么当我们单击按钮时,看起来好像什么都没有发生:



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

作为一个解释:上面的代码有错误,因为当用户单击按钮时,它不显示颜色交换。 要解决此错误,您需要将key参数添加到彩色的StatefulWidget小部件中,然后我们将根据需要交换这些小部件:



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

但这仅在您要更改的子树中具有状态的窗口小部件时才是必需的。 如果您集合中小部件的整个子树都没有状态,则不需要键。
你去! 总之,使用Flutter keys需要了解的所有内容。 但是,如果您想更深入地了解正在发生的事情...




了解为什么有时需要keys


你还在这里吧? 那么,走近一点,找出元素树和小部件的真实本质,成为Flutter Mage! 哇哈哈哈! 哈哈 哈哈 嗯对不起


如您所知,Flutter会在每个小部件内部构建相应的元素。 正如Flutter构建窗口小部件树一样,它也创建元素树(ElementTree)。 ElementTree非常简单,它仅包含每个小部件的类型信息以及到子元素的链接。 您可以将ElementTree视为Flutter应用程序的框架。 它显示了应用程序的结构,但是所有其他信息都可以在源窗口小部件的链接上找到。


上例中的行小部件包含一组用于每个子级的有序插槽。 当我们对Row中的颜色小部件重新排序时,Flutter会绕过ElementTree来检查应用程序的骨架结构是否相同。



验证从RowElement开始,然后继续到子元素。 ElementTree检查新的窗口小部件是否具有与旧窗口小部件相同的类型和key ,如果是,则元素更新其到新窗口小部件的链接。 在代码的无状态版本中,小部件没有key ,因此Flutter仅检查类型。 (如果一次有太多信息,请参见上面的动画图表。)


在ElementTree下面的状态小部件看起来有些不同。 和以前一样,有一些小部件和元素,但是也有一些状态对象用于小部件,并且颜色信息存储在其中,而不是存储在小部件本身中。



对于没有key的彩色StatefulWidget小部件,当我更改两个小部件的顺序时,Flutter会绕过ElementTree,检查RowWidget的类型并更新链接。 然后,颜色小部件元素将检查相应的小部件是否具有相同类型,并更新链接。 第二个小部件也会发生相同的情况。 由于Flutter使用ElementTree及其对应的状态来确定在设备上实际显示的内容,因此从我们的角度来看,似乎尚未交换小部件!



在彩色小部件中带有状态符的固定版本的代码中,我定义了key属性。 现在,如果我们更改Row的小部件, Row类型它们将像以前一样匹配,但是颜色小部件的key和ElementTree中的相应元素将不同。 这会导致Flutter从第一个与key不匹配的元素开始,停用color widget的这些元素,并在ElementTree中删除指向它们的链接。



然后Flutter用相应的key在ElementTree的Row元素中搜索小部件。 如果匹配,则将链接添加到小部件元素。 Flutter为每个没有链接的孩子做。 现在Flutter将显示我们期望的颜色,当我单击按钮时,颜色小部件将更改位置。


因此,如果您使用集合中的状态更改小部件的顺序或数量,则keys非常有用。 在此示例中,我保存了颜色。 但是,情况通常不是很明显。 播放动画,显示用户输入并滚动浏览位置-一切都有状态。




我什么时候应该使用keys


简短的回答:如果需要向应用程序添加keys ,则应将keys添加到小部件子树的顶部,并具有要保存的状态。


我看到的一个常见错误是,人们认为他们只需要为具有状态的第一个窗口小部件定义key ,但是存在细微差别。 不相信我吗? 为了显示我们可能遇到的麻烦,我将颜色小部件包装在了Padding小部件中,同时保留了颜色小部件的键。


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

现在,只需按一下按钮,小部件即可获得完全随机的颜色!



这是添加了Padding小部件的小部件树和ElementTree的外观:



当我们更改子窗口小部件的位置时,元素和窗口小部件之间的匹配算法在元素树中看起来是一级。 在该图中,孩子的孩子们变暗了,所以没有什么会分散第一层的注意力。 在此级别上,所有内容均正确匹配。



在第二级,Flutter注意到color元素的key小部件的key不匹配,因此它停用了该元素,将其丢弃并删除了所有与它的链接。 在此示例中,我们使用keysLocalKeys 。 这意味着在将小部件与元素匹配时,Flutter仅在树的特定级别上查找keys


由于无法使用相应的key在此级别找到颜色小部件元素,因此他创建了一个新的部件并初始化了新的状态,在这种情况下,使小部件变为橙色!



如果我们为Padding小部件定义keys


 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注意到了问题并正确更新了链接。 Universe中的顺序将恢复。





我应该使用哪种类型的Key


Flutter API使我们可以选择几个Key类。 您应该使用的key类型取决于需要keys的元素的区别特征。 查看您存储在各个小部件中的信息。


考虑以下待办事项应用程序[1],您可以在其中基于优先级更改任务列表中项目的顺序,完成后可以将其删除。



值键
在这种情况下,我们可以预期,关于实施的段落的文本将是永久且唯一的。 如果是这样,那么这可能是ValueKey (文本为“值”)的一个很好的候选者。


 return TodoItem( key: ValueKey(todo.task), todo: todo, onDismissed: (direction) => _removeTodo(context, todo), ); 

对象键
或者,您可能具有通讯簿应用程序,其中列出了有关每个用户的信息。 在这种情况下,每个子窗口小部件都存储更复杂的数据组合。 任何单个字段(例如,姓名或生日)都可能与另一个条目重合,但组合是唯一的。 在这种情况下, ObjectKey最有可能是最合适的。



唯一键
如果集合中有多个具有相同值的窗口小部件,或者如果您确实要确保每个窗口小部件都不同于其他所有窗口小部件,则可以使用UniqueKey 。 我在示例应用程序中使用了UniqueKey来切换颜色,因为我们没有其他常量数据会存储在我们的小部件中,而且我们不知道小部件在创建时将具有什么颜色。


但是,您不想用作key一件事是随机数。 每次创建窗口小部件时,都会生成一个新的随机数,并且您将失去帧之间的一致性。 在这种情况下,您可能根本不使用keys


PageStorageKeys
PageStorageKeys是专用keys ,包含滚动的当前状态,因此应用程序可以将其保存以备后用。



全局密钥
有两种使用GlobalKeys选项:它们使小部件可以在应用程序中的任何位置更改父级而不会丢失状态,并且可以用于访问与小部件树完全不同的部分中的另一个小部件的信息。 作为第一种情况的示例,您可以想象要在两个不同的屏幕上显示相同的窗口小部件,但是状态相同,为了保存窗口小部件数据,您将使用GlobalKey 。 在第二种情况下,当您需要检查密码,但又不想与树中的其他窗口小部件共享状态信息时,可能会出现这种情况。 GlobalKeys对于测试也非常有用,它使用key访问特定的小部件并请求有关其状态的信息。



通常(但并非总是如此!) GlobalKeys有点像全局变量。 通常,它们可以被InheritedWidgets或Redux或BLoC模板替换。




简要结论


通常,如果要维护小部件子树之间的状态,请使用“ Keys 。 当您更改相同类型的小部件的集合时,通常会发生这种情况。 将key放在要保存的小部件子树的顶部,然后根据存储在小部件中的数据选择key类型。


恭喜,您现在正准备成为Flutter Mage! 哦,我说魔术师? 我的意思是魔术师[2],就像编写应用程序源代码的人一样……几乎一样好。 ...差不多。


[1]编写此处收到的待办事项应用程序代码的启示
https://github.com/brianegan/flutter_architecture_samples/tree/master/vanilla
[2]作者使用“ sorcerer ”一词,后来在sourcerer之前添加了一个额外的字母

Source: https://habr.com/ru/post/zh-CN446050/


All Articles