Flutter key localKey globalKey 深入理解

1. 什么是key

您可以使用key来控制框架将在widget重建时与哪些其他widget匹配。默认情况下,框架根据它们的runtimeType和它们的显示顺序来匹配。使用key时,框架要求两个widget具有相同的key和runtimeType。

  • 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是否需要更新。返回true时可以更新对element索引
canUpdate 对两个(新老) Widget 的 runtimeType 和 key 进行比较,从而判断出当前的 Element 是否需要更新

If the widgets have no key (their key is null), then they are considered a match if they have the same type, even if their children are completely different.

那么设么时候用key呢?

2. key的使用

实例

  • statelessWidget 实现两个颜色块交换
class StatelessContainer extends StatelessWidget {
  final Color randomCol = getRandomColor();
  @override
  Widget build(BuildContext context) {
    return Container(
      width: 100,
      height: 100,
      color: randomCol,
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);
  final String title;
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
List<Widget> widgets;

  //初始化加载资源
  @override
  void initState() {
    super.initState();
    widgets =  [
      StatelessContainer(),
      StatelessContainer()];
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: widgets,
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: switchWidget,
        tooltip: 'change',
        child: Icon(Icons.import_export),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
  //点击事件
  switchWidget(){
    setState(() {
      widgets.insert(0, widgets.removeAt(1));
    });
  }
}

运行效果:
Flutter key localKey globalKey 深入理解_第1张图片

  • statefullWidget 实现两个颜色块交换
class StatefulContainer extends StatefulWidget {
  @override
  _StatefulContainerState createState() => _StatefulContainerState();
}

class _StatefulContainerState extends State<StatefulContainer> {
  final Color randomCol = getRandomColor();
  @override
  Widget build(BuildContext context) {
    return Container(
      width: 100,
      height: 100,
      color: randomCol,
    );
  }
}

将widges list 中的 statelessContainer 更换为StatefulContainer
运行效果:
Flutter key localKey globalKey 深入理解_第2张图片

为什么statelessWidget 可以更换, 而 statefullWidget 却不行呢?

因为flutter视图模型是树形结构,想找到原因就必须清楚flutter对widget的更新原理,还有statelessWidget和statefulWidget及其element的树状结构。

statelessWidget

在第一种使用 StatelessWidget 的实现中,当 Flutter 渲染这些 Widgets 时,Row Widget 为它的子 Widget 提供了一组有序的插槽。对于每一个 Widget,Flutter 都会构建一个对应的 Element。构建的这个 Element Tree 相当简单,仅保存有关每个 Widget 类型的信息以及对子Widget 的引用。你可以将这个 Element Tree 当做就像你的 Flutter App 的骨架。它展示了 App 的结构,但其他信息需要通过引用原始Widget来查找。
Flutter key localKey globalKey 深入理解_第3张图片
当我们交换行中的两个色块时,Flutter 遍历 Widget 树,看看骨架结构是否相同。它从 Row Widget 开始,然后移动到它的子 Widget,Element 树检查 Widget 是否与旧 Widget 是相同类型和 Key。 如果都相同的话,它会更新对新 widget 的引用。在我们这里,Widget 没有设置 Key,所以Flutter只是检查类型。它对第二个孩子做同样的事情。所以 Element 树将根据 Widget 树进行对应的更新。
交换之后:
Flutter key localKey globalKey 深入理解_第4张图片
为了方便我们理解我们可以模拟并打印canUpdate,runtimeType,key的值

  bool canBeUpdate(Widget oldWidget, Widget newWidget) {
    return oldWidget.runtimeType == newWidget.runtimeType
        && oldWidget.key == newWidget.key;
  }
  //点击事件
  switchWidget(){
    setState(() {
    bool canUpdate = canBeUpdate(widgets[0], widgets[1]);
    print(canUpdate);

    Type type1 = widgets[0].runtimeType;
    Type type2 = widgets[1].runtimeType;
    Key key1 = widgets[0].key;
    Key key2 = widgets[1].key;
    
    print(type1);
    print(type2);
    print(key1);
    print(key2);
    widgets.insert(0, widgets.removeAt(1));
    });
  }

打印结果:

flutter: true
flutter: StatelessContainer
flutter: StatelessContainer
flutter: null
flutter: null

当 Element Tree 更新完成后,Flutter 将根据 Element Tree 构建一个 Render Object Tree,最终开始渲染流程
Flutter key localKey globalKey 深入理解_第5张图片

StatefulWidget

当使用 StatefulWidget 实现时,控件树的结构也是类似的,但是statefulWidget和state是相互独立的, color 信息没有存储到statefulWidget中,而是在外部的 State 对象中。
Flutter key localKey globalKey 深入理解_第6张图片
现在,我们点击按钮,交换widget控件的次序,Flutter 将遍历 Element 树,检查 Widget 树中 Row 控件并且更新 Element 树中的引用,然后第一个 Tile 控件检查它对应的element控件是否是相同类型,它发现对方是相同的类型; 然后第二个 Tile 控件做相同的事情,最终就导致Flutter 认为这两个控件都没有发生改变(子节点state改变了)。Flutter 使用 Element 树和它对应的控件的 State 去确定要在设备上显示的内容, 所以 Element 树没有改变,显示的内容也就不会改变。
Flutter key localKey globalKey 深入理解_第7张图片
此时控制台打印:

lutter: true
flutter: StatefulContainer
flutter: StatefulContainer
flutter: null
flutter: null

解决:

statefulWidget 结合key
class MyHomePage extends StatefulWidget {
  //接受key
  MyHomePage({Key key, this.title}) : super(key: key);
  final String title;
  @override
  _MyHomePageState createState() => _MyHomePageState();
}
  void initState() {
    super.initState();
    widgets =  [
       //传入uniqueKey 
       StatefulContainer(key: UniqueKey(),),
       StatefulContainer(key: UniqueKey(),)
      ];
  }

再次运行后可以成功交换.

添加key点击后的图像:
Flutter key localKey globalKey 深入理解_第8张图片
控制台打印

flutter: false
flutter: StatefulContainer
flutter: StatefulContainer
flutter: [#a1573]
flutter: [#b36d6]

分析: 因为key不一样所以canUpdate返回false,不能更新对element的索引。原因是widget的顶点互换了,而对应的elment并没有,他们的key对应不上
Flutter key localKey globalKey 深入理解_第9张图片
只有类型和key 都匹配时,才算找到对应的 Widget。于是在 Widget Tree 发生交换后,Element Tree 中子控件和原始控件对应关系就被打乱了,所以 Flutter 会重建 Element Tree,直到控件们正确对应上
Flutter key localKey globalKey 深入理解_第10张图片
所以,现在 Element 树正确更新了,最终就会显示交换后的色块。

Flutter key localKey globalKey 深入理解_第11张图片
控制台打印:

flutter: false
flutter: StatefulContainer
flutter: StatefulContainer
flutter: [#b36d6]
flutter: [#a1573]

分析:element tree 根据key 找到了对应的element索引,并更改了element tree 的结构.

3.key的使用位置

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

我们在init列表时给列表一个间距,使statefulWidget有一个父节点

  @override
  void initState() {
    super.initState();
    widgets =  [
     Padding(//间距
        padding: const EdgeInsets.all(8.0),
        child: StatefulContainer(key: UniqueKey(),),
      ),
      Padding(
        padding: const EdgeInsets.all(8.0),
        child: StatefulContainer(key: UniqueKey(),),
      )
      ];
  }

从新运行后效果:
Flutter key localKey globalKey 深入理解_第12张图片
此时不再交换颜色而是创建了一个新的色块来替换之前的。

why?

首先:widget数组交换位置时,padding会携带其子节点作为整体进行交换,此图是交换完成时
Flutter key localKey globalKey 深入理解_第13张图片
此时,canUpdate只检查父节点的runtimetype和key,不关心其子节点的内容(只比较树中的同一层级)
此时控制台打印

lutter: true
flutter: Padding
flutter: Padding
flutter: null
flutter: null

此时canUpdate返回true,因为Padding层widget并没有发生本质的变化,所以更新两个树(widget & element)中对应的索引。
Flutter key localKey globalKey 深入理解_第14张图片
随后flutter开始进行第二层对比,在对比时Flutter发现元素与组件的Key并不匹配,于是,把它设置成不可用状态,但是这里所使用的Key只是本地Key(Local Key),Flutter并不能找到另一层里面的Key(即另外一个Padding Widget中的Key)所以,Flutter就创建了一个新的 Element 并初始化一个新 State,而这个Widget的颜色就成了我们看到的『随机色』。  
Flutter key localKey globalKey 深入理解_第15张图片
由此可见,key只有放到父节点(此demo中的padding)上,widget节点才能正确匹配找到对应信息的element节点,解决这个问题: 将 Key 设置到上层 Widget Padding 上

  @override
  void initState() {
    super.initState();
    widgets =  [
     Padding(
        key: UniqueKey(),
        padding: const EdgeInsets.all(8.0),
        child: StatefulContainer(),
      ),
      Padding(
        key: UniqueKey(),
        padding: const EdgeInsets.all(8.0),
        child: StatefulContainer(),
      )
      ];
  }

4.Kye的种类

Key 派生出两种不同用途的 Key:LocalKey 和 GlobalKey。

Flutter key localKey globalKey 深入理解_第16张图片

Localkey

LocalKey 直接继承至 Key,它应用于拥有相同父 Element 的小部件进行比较的情况,也就是上述例子中,有一个多子 Widget 中需要对它的子 widget 进行移动处理,这时候你应该使用Localkey
Localkey 派生出了许多子类 key:

  • ValueKey : ValueKey(‘String’)
  • ObjectKey : ObjectKey(Object)
  • UniqueKey : UniqueKey()
       - Valuekey 又派生出了 PageStorageKey : PageStorageKey(‘value’)
GlobalKey

GlobalKey 使用了一个静态常量 Map 来保存它对应的 Element。你可以通过 GlobalKey 找到持有该GlobalKey的 Widget,State 和 Element。 Global即可以在多个页面或者层级复用,比如两个页面也可也同时保持一个状态

注意:GlobalKey 是非常昂贵的,需要谨慎使用。

5.key的使用

ValueKey

例如在一个 ToDo 列表应用中,每个 Todo Item 的文本是恒定且唯一的。这种情况,适合使用 ValueKey,value 是文本。
如果您有一个 Todo List 应用程序,它将会记录你需要完成的事情。我们假设每个 Todo 事情都各不相同,而你想要对每个 Todo 进行滑动删除操作。

return TodoItem(
    key: ValueKey(todo.task),
    todo: todo,
    onDismissed: (direction){
        _removeTodo(context, todo);
    },
);
ObjectKey

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

UniqueKey

如果集合中有多个具有相同值的 Widget,或者如果您想确保每个 Widget 与其他 Widget 不同,则可以使用 UniqueKey。 在我们的例子中就使用了 UniqueKey,因为我们没有将任何其他常量数据存储在我们的色块上,并且在构建 Widget 之前我们不知道颜色是什么。
不要在 Key 中使用随机数,如果你那样设置,那么当每次构建 Widget 时,都会生成一个新的随机数,Element 树将不会和 Widget 树做一致的更新。使用场景比较少
真实的开发中,我们可以用Model中的id作为ObjectKey。

PageStorageKey

当你有一个滑动列表,你通过某一个 Item 跳转到了一个新的页面,当你返回之前的列表页面时,你发现滑动的距离回到了顶部。这时候,给 Sliver 一个 PageStorageKey 它将能够保持 Sliver 的滚动状态。

GlobalKey

GlobalKey 能够跨 Widget 访问状态,我们想要在外部改变该状态,这时候就需要使用 GlobalKey
示例:

class SwitchScreen extends StatefulWidget {
  //接受key
  SwitchScreen({Key key}) : super(key: key);
  _SwitchScreenState createState() => _SwitchScreenState();
}

class _SwitchScreenState extends State<SwitchScreen> {
  bool isActive = false;
  @override
  Widget build(BuildContext context) { 
    return Switch.adaptive(
      value: isActive,
      onChanged: (bool currentState){
        setState(() {
          isActive = currentState;
        });
      },
    );
  }
  changeState() {
    setState(() {
      isActive = !isActive;
    });
  }
}

初始化并传入globalkey实现跨widget访问

class _MyHomePageState extends State<MyHomePage> {
  //global key
  final GlobalKey<_SwitchScreenState> gKey = GlobalKey<_SwitchScreenState>();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        //global key
        child: SwitchScreen(key: gKey,),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: switchSwitch,  //更改switch状态
        tooltip: 'change',
        child: Icon(Icons.import_export),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
  //点击事件 global key
  switchSwitch() {
    setState(() {
      gKey.currentState.changeState();
    });
  }
  
}

实现效果:
Flutter key localKey globalKey 深入理解_第17张图片

6.总结

如何合理适当的使用 Key:

  • When: 当您想要保留 Widget 树的状态时,请使用 Key。例如: 当修改相同类型的 Widget 集合(如列表中)时
  • Where: 将 Key 设置在要指明唯一身份的 Widget 树的顶部
  • Which: 根据在该 Widget 中存储的数据类型选择使用的不同类型的Key

demo代码
参考链接:
https://www.cnblogs.com/lxlx1798/p/11171636.html
https://www.jianshu.com/p/6e704112dc67
https://flutterchina.club/widgets-intro/

你可能感兴趣的:(Flutter)