一,前言
在开发 Flutter 的过程中你可能会发现,一些小部件的构造函数中都有一个可选的参数——Key。在这篇文章中我们会深入浅出的介绍什么是 Key,以及应该使用 key 的具体场景。
二,什么是Key
在 Flutter 中我们经常与状态打交道。我们知道 Widget 可以有 Stateful 和 Stateless 两种。Key 能够帮助开发者在 Widget tree 中保存状态,在一般的情况下,我们并不需要使用 Key。那么,究竟什么时候应该使用 Key呢。
我们来看看下面这个例子。
class StatelessContainer extends StatelessWidget { final Color color = RandomColor().randomColor(); @override Widget build(BuildContext context) { return Container( width: 100, height: 100, color: color, ); } }
这是一个很简单的 Stateless Widget,显示在界面上的就是一个 100 * 100 的有颜色的 Container。 RandomColor 能够为这个 Widget 初始化一个随机颜色。
我们现在将这个Widget展示到界面上。
class Screen extends StatefulWidget { @override _ScreenState createState() => _ScreenState(); } class _ScreenState extends State<Screen> { List<Widget> widgets = [ StatelessContainer(), StatelessContainer(), ]; @override Widget build(BuildContext context) { return Scaffold( body: Center( child: Row( mainAxisAlignment: MainAxisAlignment.center, children: widgets, ), ), floatingActionButton: FloatingActionButton( onpressed: switchWidget, child: Icon(Icons.undo), ), ); } switchWidget(){ widgets.insert(0, widgets.removeAt(1)); setState(() {}); } }
这里在屏幕中心展示了两个 StatelessContainer 小部件,当我们点击 floatingActionButton 时,将会执行 switchWidget 并交换它们的顺序。
看上去并没有什么问题,交换操作被正确执行了。现在我们做一点小小的改动,将这个 StatelessContainer 升级为 StatefulContainer。
class StatefulContainer extends StatefulWidget { StatefulContainer({Key key}) : super(key: key); @override _StatefulContainerState createState() => _StatefulContainerState(); } class _StatefulContainerState extends State<StatefulContainer> { final Color color = RandomColor().randomColor(); @override Widget build(BuildContext context) { return Container( width: 100, height: 100, color: color, ); } }
在 StatefulContainer 中,我们将定义 Color 和 build方法都放进了 State 中。
现在我们还是使用刚才一样的布局,只不过把 StatelessContainer 替换成 StatefulContainer,看看会发生什么。
这时,无论我们怎样点击,都再也没有办法交换这两个Container的顺序了,而 switchWidget 确实是被执行了的。
为了解决这个问题,我们在两个 Widget 构造的时候给它传入一个 UniqueKey。
class _ScreenState extends State<Screen> { List<Widget> widgets = [ StatefulContainer(key: UniqueKey(),), StatefulContainer(key: UniqueKey(),), ]; ···
然后这两个 Widget 又可以正常被交换顺序了。
看到这里大家肯定心中会有疑问,为什么 Stateful Widget 无法正常交换顺序,加上了 Key 之后就可以了,在这之中到底发生了什么? 为了弄明白这个问题,我们将涉及 Widget 的 diff 更新机制。
-
Widget 更新机制
下面来来看Widget的源码。@immutable abstract class Widget extends DiagnosticableTree { const Widget({ this.key }); final Key key; ··· static bool canUpdate(Widget oldWidget, Widget newWidget) { return oldWidget.runtimeType == newWidget.runtimeType && oldWidget.key == newWidget.key; } }
我们知道 Widget 只是一个配置且无法修改,而 Element 才是真正被使用的对象,并可以修改。当新的 Widget 到来时将会调用 canUpdate 方法,来确定这个 Element是否需要更新。
canUpdate 对两个(新老) Widget 的 runtimeType 和 key 进行比较,从而判断出当前的 Element 是否需要更新。
-
-
StatefulContainer 比较过程
而在 StatefulContainer 的例子中,我们将 color 的定义放在了 State 中,Widget 并不保存 State,真正 hold State 的引用的是 Stateful Element。当我们没有给 Widget 任何 key 的时候,将会只比较这两个 Widget 的 runtimeType 。由于两个 Widget 的属性和方法都相同,canUpdate 方法将会返回 false,在 Flutter 看来,并没有发生变化。所以这两个 Element 将不会交换位置。而我们给 Widget 一个 key 之后,canUpdate 方法将会比较两个 Widget 的 runtimeType 以及 key。并返回 true,现在 Flutter 就可以正确的感知到两个 Widget 交换了顺序了。 (这里 runtimeType 相同,key 不同)
-
-
比较范围
为了提升性能 Flutter 的比较算法(diff)是有范围的,它并不是对第一个 StatefulWidget 进行比较,而是对某一个层级的 Widget 进行比较。··· class _ScreenState extends State<Screen> { List<Widget> widgets = [ Padding( padding: const EdgeInsets.all(8.0), child: StatefulContainer(key: UniqueKey(),), ), Padding( padding: const EdgeInsets.all(8.0), child: StatefulContainer(key: UniqueKey(),), ), ]; ···
在这个例子中,我们将两个带 key 的 StatefulContainer 包裹上 Padding 组件,然后点击交换按钮,会发生下面这件奇妙的事情。
结论:
两个 Widget 的 Element 并不是交换顺序,而是被重新创建了。
分析:
在 Flutter 的比较过程中它下到 Row 这个层级,发现它是一个 MultiChildRenderObjectWidget(多子部件的 Widget)。然后它会对所有 children 层逐个进行扫描。
首先它会查看第一个 padding,发现 padding 部分的 runtimeType 并没有改变。然后再比较第一个 padding 内部的 Widget,由于内部的 Widget 存在 key,并且现在的 key 应该是之前第二个 StatefulContainer 的 key,和原来的(第一个
StatefulContainer 的 key)对比发生了变化。且现在的层级在 padding 内部,该层级没有多子 Widget。Flutter 的比较算法将会认为这个 Element 被替换了。将会重新生成一个新的 Element 对象装载到 Element 树上。
然后 Flutter 继续在 Row 的 children 中继续往下查看下面的部件,第二个同理。
所以为了解决这个问题,我们需要将 key 放到 Row 的 children 这一层级。
··· class _ScreenState extends State<Screen> { List<Widget> widgets = [ Padding( key: UniqueKey(), padding: const EdgeInsets.all(8.0), child: StatefulContainer(), ), Padding( key: UniqueKey(), padding: const EdgeInsets.all(8.0), child: StatefulContainer(), ), ]; ···
现在我们又可以愉快的玩耍了(交换 Widget 顺序)了。
三,Key 的种类
-
Key
@immutable abstract class Key { const factory Key(String value) = ValueKey<String>; @protected const Key.empty(); }
默认创建 Key 将会通过工厂方法根据传入的 value 创建一个 ValueKey。
Key 派生出两种不同用途的 Key:LocalKey 和 GlobalKey。
-
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
如果您有一个 Todo List 应用程序,它将会记录你需要完成的事情。我们假设每个 Todo 事情都各不相同,而你想要对每个 Todo 进行滑动删除操作。
这时候就需要使用 ValueKey
return TodoItem( key: ValueKey(todo.task), todo: todo, ondismissed: (direction){ _removetodo(context, todo); }, );
-
ObjectKey
如果你有一个生日应用,它可以记录某个人的生日,并用列表显示出来,同样的还是需要有一个滑动删除操作。
我们知道人名可能会重复,这时候你无法保证给 Key 的值每次都会不同。但是,当人名和生日组合起来的 Object 将具有唯一性。
这时候你需要使用 ObjectKey!
-
UniqueKey
如果组合的 Object 都无法满足唯一性的时候,你想要确保每一个 Key 都具有唯一性。那么,你可以使用 UniqueKey。它将会通过该对象生成一个具有唯一性的 hash 码。
不过这样做,每次 Widget 被构建时都会去重新生成一个新的 UniqueKey,失去了一致性。也就是说你的小部件还是会改变。(还不如不用?)
-
PageStorageKey
当你有一个滑动列表,你通过某一个 Item 跳转到了一个新的页面,当你返回之前的列表页面时,你发现滑动的距离回到了顶部。这时候,给 Sliver 一个 PageStorageKey 它将能够保持 Sliver 的滚动状态。
-
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<SwitcherScreenState> 并传递给 SwitcherScreen。然后我们便可以通过这个 key 拿到它所绑定的 SwitcherState 并在外部调用 changeState 改变状态了。
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 [email protected] 举报,一经查实,本站将立刻删除。