Flutter之Key

Key is an identifier for Widgets, Elements and SemanticsNodes.

key是Widgets, Elements and SemanticsNodes的标识符

key在具有相同父级的元素中,必须是唯一的

 一个交换色块的例子:

随机生成两个无状态的色块,点击浮动按钮,交换两个色块的位置-成功

import 'dart:math';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';

main()=>runApp(MyApp());

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: "key——>transfer color",
      home: keyTransfer(),
    );
  }
}
class keyTransfer extends StatefulWidget {
  const keyTransfer({Key? key}) : super(key: key);

  @override
  State<keyTransfer> createState() => _keyTransferState();
}

class _keyTransferState extends State<keyTransfer> {
  List<Widget> colorPan=[];
  List<Widget> colorPan2=[];

  @override
  void initState() {
    super.initState();
    colorPan=[
      StatelessContainer(),
      StatelessContainer(),
    ];
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Container(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: colorPan,
              ),
            ],
          ),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: switchWidget,
        child: Icon(Icons.undo),
      ),
    );
  }

  //将colorPan的第一个颜色插入到第二个位置,并且使用setStateflutter框架重新渲染屏幕
  switchWidget(){
    colorPan.insert(0, colorPan.removeAt(1));
    setState(() {});
  }
}

class StatelessContainer extends StatelessWidget {

  final Color color=Color.fromRGBO(Random().nextInt(256), Random().nextInt(256), Random().nextInt(256), 1);
  @override
  Widget build(BuildContext context) {
    return Container(
      width: 100,
      height: 100,
      color: color,
    );
  }
}



首先我们来看,为什么无状态部件会更新。这不得不提到Widget源码中的canUpdate方法:

该方法用于判断给定的两个 Widget 对象是否可以用于更新一个已经存在的 Element 对象。

/// Whether the `newWidget` can be used to update an [Element] that currently
  /// has the `oldWidget` as its configuration.
  ///
  /// An element that uses a given widget as its configuration can be updated to
  /// use another widget as its configuration if, and only if, the two widgets
  /// have [runtimeType] and [key] properties that are [operator==].
  ///
  /// 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.
  static bool canUpdate(Widget oldWidget, Widget newWidget) {
    return oldWidget.runtimeType == newWidget.runtimeType
        && oldWidget.key == newWidget.key;
  }

注释翻译:

是否可以使用“newWidget”来更新当前将“oldWidget”作为其配置的[Element]。使用给定widget作为其配置的Element]可以更新为使用另一个widget作为其配置当且仅当 ,这两个widget具有相同的 [runtimeType] 和 [key] 属性,即 [operator==]。

如果小部件没有键(它们的键为 null),那么如果它们具有相同的类型,则它们被视为匹配,即使它们的 孩子们则完全不同。

由上可知,我们所给出的StatelessContainer继承自StatelessWidget Widget。

当我们点击转换按钮时,只是交换了两个StatelessContaire在Widget树中的位置却没有交换两个StatelessContaire在Element树中的位置。

为什么没有交换两个StatelessContaire在Element树中的位置?

因为在 Flutter 框架中,每个 Element 对象都有一个唯一的标识符,称为 Element 标识符。Element 标识符是由 Widget 树和 Element 树中的节点类型和位置共同决定的。当我们交换 StatelessContainer1 和 StatelessContainer2 的位置时,它们在 Widget 树中的位置发生了变化,但是它们所对应的 Element 对象的标识符并没有改变。因此,Flutter 框架可以重用原来的 Element 对象,并将新的 Widget 对象传递给它们进行更新。

在一些情况下,当我们更新 Widget 时,可能会导致它们所对应的 Element 对象也需要更新。例如,当我们更改了 Widget 的某些配置信息(例如颜色、大小、位置等)时,就需要更新它们所对应的 Element 对象中存储的配置信息。在这种情况下,Flutter 框架会创建一个新的 Element 对象,并将新的 Widget 对象传递给它进行更新。这样做的目的是确保 Element 对象中存储的配置信息与 Widget 对象中的配置信息保持一致。

再加上此时我们并没有为这两个 StatelessContainer 配置key键,并且此时的两个widget具有相同的runtimeType属性,所以可以使用“newWidget”来更新当前将“oldWidget”作为其配置的[Element]

即,我们可以重用 StatelessContainer1 和 StatelessContainer2 的Element,用 StatelessContainer1 的widget更新 StatelessContainer2的element,反之亦然。

将色块变为有状态类型后-交换失败

代码如下:

import 'dart:math';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';

main()=>runApp(MyApp());

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: "key——>transfer color",
      debugShowCheckedModeBanner: false,
      home: keyTransfer(),
    );
  }
}
class keyTransfer extends StatefulWidget {
  const keyTransfer({Key? key}) : super(key: key);

  @override
  State<keyTransfer> createState() => _keyTransferState();
}

class _keyTransferState extends State<keyTransfer> {
  List<Widget> colorPan=[];
  List<Widget> colorPan2=[];

  @override
  void initState() {
    super.initState();
    colorPan2=[
      StatefulContainer(),
      StatefulContainer(),
    ];
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Container(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
             Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Text("有状态——>"),
                  SizedBox(width: 30,),
                  Row(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: colorPan2,
                  ),
                ],
              )
            ],
          ),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: switchWidget,
        child: Icon(Icons.undo),
      ),
    );
  }

  //将colorPan的第一个颜色插入到第二个位置,并且使用setStateflutter框架重新渲染屏幕
  switchWidget(){
   colorPan2.insert(0, colorPan2.removeAt(1));
    setState(() {});
  }
}



class StatefulContainer extends StatefulWidget {
  const StatefulContainer({Key? key}) : super(key: key);

  @override
  State<StatefulContainer> createState() => _StatefulContainerState();
}

class _StatefulContainerState extends State<StatefulContainer> {
  final Color color=Color.fromRGBO(Random().nextInt(256), Random().nextInt(256), Random().nextInt(256), 1);
  @override
  Widget build(BuildContext context) {
    return Container(
      width: 100,
      height: 100,
      color: color,
    );
  }
}



Flutter之三棵树中提到:

 StatefulWidget 有一个State对象,colo属性存储在这里而不是存储在Widget本身。StatefulWidget的widget是不可变的,但State是可变的。

官方关于state:

State is information that (1) can be read synchronously when the widget is built and (2) might change during the lifetime of the widget. It is the responsibility of the widget implementer to ensure that the State is promptly notified when such state changes, using State.setState.

状态是以下信息:

(1) 在构建小部件时可以同步读取;

(2) 在小部件的生命周期中可能会发生变化。 小部件实现者有责任确保在状态更改时使用 State.setState 及时通知状态。

点击浮动按钮交换 StatefulContainer1 和 StatefulContainer2在Widget树中的位置时,他们在Element树的位置没有发生改变,同样的state也没有发生更改。所以点击浮动按钮,两个色块颜色没有交换成功。

为有状态色块中添加上key后-交换成功

@override
  void initState() {
    super.initState();
    colorPan=[
      StatelessContainer(),
      StatelessContainer(),
    ];
    colorPan2=[
      StatefulContainer(key: UniqueKey(),),
      StatefulContainer(key: UniqueKey(),),
    ];
  }

点击浮动按钮交换 StatefulContainer1 和 StatefulContainer2 在Widget树中的位置时,Flutter框架会检查Element树中对应位置的 StatefulContainer2 的Widget和 StatefulContainer1 的element中的Widget的key属性是否匹配。

很明显此时并不匹配,则 canUpdate 方法会返回 false,表示当前 Element 对象不能被更新。所以 Flutter 会停用这些元素,并从第一个不匹配的 Element 开始查找具有相同 Key 的 Widget。如果找到了,则重新绑定 State 对象,更新状态并调用 setState 方法,通知 Flutter 框架重新构建小部件以更新 UI。

在哪里使用key?

当我们将 StatefulContainer用Padding包裹起来时,再点击交换按钮,我们会发现此时并不是交换两个色块,而是重新创建了两个色块。

import 'dart:math';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';

main()=>runApp(MyApp());

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: "key——>transfer color",
      debugShowCheckedModeBanner: false,
      home: keyTransfer(),
    );
  }
}
class keyTransfer extends StatefulWidget {
  const keyTransfer({Key? key}) : super(key: key);

  @override
  State<keyTransfer> createState() => _keyTransferState();
}

class _keyTransferState extends State<keyTransfer> {
  List<Widget> colorPan=[];
  List<Widget> colorPan2=[];

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Container(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              /*Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Text("无状态——>"),
                  SizedBox(width: 30,),
                  Row(
                    mainAxisAlignment:  MainAxisAlignment.spaceBetween,
                    children: colorPan,
                  ),
                  SizedBox(height: 20,),
                ],
              ),*/
              SizedBox(height: 20,),
              Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Text("有状态——>"),
                  SizedBox(width: 30,),
                  Row(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: colorPan2,
                  ),
                ],
              )
            ],
          ),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: switchWidget,
        child: Icon(Icons.undo),
      ),
    );
  }

  //将colorPan的第一个颜色插入到第二个位置,并且使用setStateflutter框架重新渲染屏幕
  switchWidget(){
    colorPan.insert(0, colorPan.removeAt(1));
    colorPan2.insert(0, colorPan2.removeAt(1));
    setState(() {});
  }
}

class StatefulContainer extends StatefulWidget {
  const StatefulContainer({Key? key}) : super(key: key);

  @override
  State<StatefulContainer> createState() => _StatefulContainerState();
}

class _StatefulContainerState extends State<StatefulContainer> {
  final Color color=Color.fromRGBO(Random().nextInt(256), Random().nextInt(256), Random().nextInt(256), 1);
  @override
  Widget build(BuildContext context) {
    return Container(
      width: 100,
      height: 100,
      color: color,
    );
  }
}



这是为什么? 

在 Flutter 的比较过程中它下到 Row 这个层级,发现它是一个 MultiChildRenderObjectWidget(多子部件的 Widget)。然后它会对所有 children 层逐个进行扫描。

在padding 层时,runtimeType 并没有改变,且不存在 Key,因此再比较下一个层级。

由于内部的 StatefulContainer 存在新旧不同的key,且现在的层级在 padding 内部,该层级没有其他的子 Widget。所以canUpdate 返回 flase后,Flutter 将会认为这个 Element 需要被替换。然后重新生成一个新的 Element 对象装载到 Element 树上替换掉之前的 Element。第二个 Widget 同理。

所以,key应该被放置在组件树的顶层。

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

Key的分类

LocalKey 

A key that is not a GlobalKey.

Keys must be unique amongst the Elements with the same parent. By contrast, GlobalKeys must be unique across the entire app.

在具有相同父级的Element中,键必须是唯一的。相比之下,GlobalKey在整个应用程序中必须是唯一的。

  • ObjectKey  一个键,它从用作其值的对象中获取其标识。
  • UniqueKey  一个只等于它本身的密钥。
  • ValueKey   使用特定类型的值来标识自身的键。
  • PageStorageKey:专用于存储页面滚动位置的key。

key的默认构造函数,默认的是ValueKey:

@immutable
abstract class Key {
  /// Construct a [ValueKey<String>] with the given [String].
  ///
  /// This is the simplest way to create keys.
  const factory Key(String value) = ValueKey<String>;

  /// Default constructor, used by subclasses.
  ///
  /// Useful so that subclasses can call us, because the [Key.new] factory
  /// constructor shadows the implicit constructor.
  @protected
  const Key.empty();
}

GlobalKey

A key that is unique across the entire app.

Global keys uniquely identify elements. Global keys provide access to other objects that are associated with those elements, such as BuildContext. For StatefulWidgets, global keys also provide access to State.

在整个应用程序中唯一的密钥。

全局键唯一标识元素。 全局键提供对与这些元素关联的其他对象的访问,例如 BuildContext。 对于 StatefulWidgets,全局键还提供对 State 的访问。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值