理解 Flutter 中的 Key

参考:https://www.jianshu.com/p/6e704112dc67

概览

在Flutter中,大概大家都知道如何更新界面视图: 通过修改State去触发Widget重建,触发和更新的操作是Flutter框架做的。 但是有时即使修改了State,Flutter框架好像也没有触发Widget重建,

其中就隐含了Flutter框架内部的更新机制,在某些情况下需要结合使用Key,才能触发真正的“重建”。

下面将从 3 个方面 (When, Where, Which) 说明如何在合理的时间和地点使用合理的 Key。

When: 什么时候该使用Key

实战例子

需求: 点击界面上一个按钮,然后交换行中的两个色块。

StatelessWidget 实现

使用StatelessWidget(StatelessColorfulTile) 做child(tiles):

classPositionedTilesextendsStatefulWidget{@overrideState<StatefulWidget>createState()=>PositionedTilesState();}classPositionedTilesStateextendsState<PositionedTiles>{List<Widget>tiles;@overridevoidinitState(){super.initState();tiles=[StatelessColorfulTile(),StatelessColorfulTile(),];}@overrideWidgetbuild(BuildContext context){returnScaffold(body:SafeArea(child:Center(child:Row(mainAxisAlignment:MainAxisAlignment.center,children:tiles))),floatingActionButton:FloatingActionButton(child:Icon(Icons.sentiment_very_satisfied),onPressed:swapTiles));}

当点击按钮时,更新PositionedTilesState中储存的tiles:

voidswapTiles(){setState((){tiles.insert(1,tiles.removeAt(0));});}}

classStatelessColorfulTileextendsStatelessWidget{finalColor color=UniqueColorGenaretor.getColor();StatelessColorfulTile({Key key}):super(key:key);@overrideWidgetbuild(BuildContext context)=>buildColorfulTile(color);}

结果

PositionedTiles

成功实现需求_

StatefulWidget 实现

使用StatefulWidget(StatefulColorfulTile) 做child(tiles):

classStatefulColorfulTileextendsStatefulWidget{StatefulColorfulTile({Key key}):super(key:key);@overrideState<StatefulWidget>createState()=>StatefulColorfulTileState();}classStatefulColorfulTileStateextendsState<StatefulColorfulTile>{// 将 Color 储存在 StatefulColorfulTile 的 State StatefulColorfulTileState 中.Color color;@overridevoidinitState(){super.initState();color=UniqueColorGenaretor.getColor();}@overrideWidgetbuild(BuildContext context)=>buildColorfulTile(color);}

修改外部容器PositionedTiles中tiles:

@overridevoidinitState(){super.initState();tiles=[StatefulColorfulTile(),StatefulColorfulTile(),];}

结果

PositionedTiles

貌似没效果 -_-

为什么使用StatefulWidget就不能成功更新呢? 需要先了解下面的内容。

Fluuter 对 Widget 的更新原理

在 Flutter 框架中,视图维持在树的结构中,我们编写的 Widget 一个嵌套一个,最终组合为一个 Tree。

StatelessWidget

在第一种使用StatelessWidget的实现中,当 Flutter 渲染这些 Widgets 时,RowWidget 为它的子 Widget 提供了一组有序的插槽。对于每一个 Widget,Flutter 都会构建一个对应的Element。构建的这个ElementTree 相当简单,仅保存有关每个Widget类型的信息以及对子Widget的引用。你可以将这个ElementTree 当做就像你的 Flutter App 的骨架。它展示了 App 的结构,但其他信息需要通过引用原始Widget来查找。

StatelessWidget Tree & Element Tree"

当我们交换行中的两个色块时,Flutter 遍历Widget树,看看骨架结构是否相同。它从RowWidget 开始,然后移动到它的子 Widget,Element 树检查 Widget 是否与旧 Widget 是相同类型和Key。 如果都相同的话,它会更新对新 widget 的引用。在我们这里,Widget 没有设置 Key,所以Flutter只是检查类型。它对第二个孩子做同样的事情。所以 Element 树将根据 Widget 树进行对应的更新。

swap之后

当 Element Tree 更新完成后,Flutter 将根据 Element Tree 构建一个 Render Object Tree,最终开始渲染流程。

类似这样的渲染流程

StatefulWidget

当使用StatefulWidget实现时,控件树的结构也是类似的,只是现在 color 信息没有存储控件自身了,而是在外部的 State 对象中。

StatefulWidget Tree & Element Tree

现在,我们点击按钮,交换控件的次序,Flutter 将遍历 Element 树,检查 Widget 树中Row控件并且更新 Element 树中的引用,然后第一个 Tile 控件检查它对应的控件是否是相同类型,它发现对方是相同的类型; 然后第二个 Tile 控件做相同的事情,最终就导致 Flutter 认为这两个控件都没有发生改变。Flutter 使用 Element 树和它对应的控件的 State 去确定要在设备上显示的内容, 所以 Element 树没有改变,显示的内容也就不会改变。

swap之后

StatefullWidget 结合 Key

现在,为StatefulColorfulTile传递一个Key对象:

voidinitState(){super.initState();tiles=[// 使用 UniqueKeyStatefulColorfulTile(key:UniqueKey()),StatefulColorfulTile(key:UniqueKey()),];}

再次运行:

PositionedTiles

成功 swap!

添加了Key之后的结构:

PositionedTiles

当现在执行 swap 时, Element 数中 StatafulWidget 控件除了比较类型外,还会比较key是否相等:

检查比较

只有类型和key都匹配时,才算找到对应的 Widget。于是在 Widget Tree 发生交换后,Element Tree 中子控件和原始控件对应关系就被打乱了,所以 Flutter 会重建 Element Tree,直到控件们正确对应上。

重建

所以,现在 Element 树正确更新了,最终就会显示交换后的色块。

交换完毕

使用场景

如果要修改集合中的控件的顺序或数量,Key会很有用。

Where: 在哪设置 Key

正常情况下应该在当前 Widget 树的顶级 Widget 中设置。

回到StatefulColorfulTile例子中,为每个色块添加一个Padding,同时key还是设置在相同的地方:

@overridevoidinitState(){super.initState();tiles=[Padding(padding:constEdgeInsets.all(8.0),child:StatefulColorfulTile(key:UniqueKey()),),Padding(padding:constEdgeInsets.all(8.0),child:StatefulColorfulTile(key:UniqueKey()),),];}

交换时

当点击按钮发生交换之后,可以看到两个色块的颜色会随机改变,但是我的预期是两个固定的颜色彼此交换。

为什么产生问题

当Widget 树中两个Padding发生了交换,它们包裹的色块也就发生了交换:

交换

然后 Flutter 将进行检查,以便对 Element 树进行对应的更新: Flutter 的Elemetn to Widget匹配算法将一次只检查树的一个层级:

检查

在第一级,PaddingWidget 都正确匹配。

检查

在第二级,Flutter 注意到 Tile 控件的Key不匹配,就停用该 Tile Element,删除 Widget 和 Element 之间的连接

检查

我们这里使用的Key是UniqueKey, 它是一个LocalKey

LocalKey的意思是: 当 Widget 与 Element 匹配时,Flutter 只在树中特定级别内查找匹配的 Key。因此 Flutter 无法在同级中找到具有该 Key 的 Tile Widget,所以它会创建一个新 Element 并初始化一个新 State。 就是这个原因,造成色块颜色发生随机改变,每次交换相当于生成了两个新的 Widget。

解决这个问题: 将Key设置到上层 WidgetPadding上

当 Widget 树中两个Padding发生交换之后,Flutter 就能根据Padding上Key的变化,更新Element树中的两个Padding,从而实现交换。

@overridevoidinitState(){super.initState();tiles=[Padding(key:UniqueKey(),padding:constEdgeInsets.all(8.0),child:StatefulColorfulTile(),),Padding(key:UniqueKey(),padding:constEdgeInsets.all(8.0),child:StatefulColorfulTile(),),];}

根据 Padding Key

Which: 该使用哪种类型的 Key

Key的目的在于为每个 Widget 指明一个唯一的身份,使用何种Key就要依具体的使用场景决定。

ValueKey

例如在一个ToDo列表应用中,每个TodoItem 的文本是恒定且唯一的。这种情况,适合使用ValueKey,value 是文本。

ObjectKey

假设,每个子 Widget 都存储了一个更复杂的数据组合,比如一个用户信息的地址簿应用。任何单个字段(如名字或生日)可能与另一个条目相同,但每个数据组合是唯一的。在这种情况下,ObjectKey最合适。

UniqueKey

如果集合中有多个具有相同值的 Widget,或者如果您想确保每个 Widget 与其他 Widget 不同,则可以使用UniqueKey。 在我们的例子中就使用了UniqueKey,因为我们没有将任何其他常量数据存储在我们的色块上,并且在构建 Widget 之前我们不知道颜色是什么。

不要在Key中使用随机数,如果你那样设置,那么当每次构建 Widget 时,都会生成一个新的随机数,Element 树将不会和 Widget 树做一致的更新。

GlobalKeys

Global Keys有两种用途。

它们允许 Widget 在应用中的任何位置更改父级而不会丢失 State ,或者可以使用它们在 Widget 树 的完全不同的部分中访问有关另一个 Widget 的信息。

比如: 要在两个不同的屏幕上显示相同的 Widget,同时保持相同的 State,则需要使用 GlobalKeys。

复用 Widget

在第二种情况下,您可能希望验证密码,但不希望与树中的其他 Widget 共享该状态信息,可以使用GlobalKey<FromState>持有一个表单Form的State。 Flutter.dev 上有这个例子Building a form with validation

其实 GlobalKeys 看起来有点像全局变量。有也其他更好的方法达到 GlobalKeys 的作用,比如 InheritedWidget、Redux 或 Block Pattern。

总结

如何合理适当的使用Key:

When: 当您想要保留 Widget 树的状态时,请使用Key。例如: 当修改相同类型的 Widget 集合(如列表中)时

Where: 将Key设置在要指明唯一身份的 Widget 树的顶部

Which: 根据在该 Widget 中存储的数据类型选择使用的不同类型的Key

作者:stefanJi

链接:https://www.jianshu.com/p/6e704112dc67

来源:简书

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335