美文网首页flutterFlutterflutter
二、Flutter中Key的作用

二、Flutter中Key的作用

作者: Mlqq | 来源:发表于2021-03-23 17:22 被阅读0次

    Flutter中每个Widget的构造方法都提供了一个可选参数Key,这个Key有什么用呢?

    1、案例

    现在看一个小小的Demo,这个Demo实现的功能是:每点击一次删除按钮,移除屏幕上的第一个Widget,功能非常简单,代码如下:

    void main() {
      runApp(MyApp());
    }
    
    class MyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Flutter Demo',
          theme: ThemeData(
    
            primarySwatch: Colors.blue,
          ),
          home: MyHomePage(),
        );
      }
    }
    
    class MyHomePage extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return DeleteItem();
      }
    }
    class DeleteItem extends StatefulWidget {
    
      @override
      _DeleteItemState createState() => _DeleteItemState();
    }
    class _DeleteItemState extends State<DeleteItem> {
    
      List <Item> _itemList = [
        Item('AAAAAA'),
        Item('BBBBBB'),
        Item('CCCCCC'),
        Item('DDDDDD'),
      ];
       void _deleteItem() {
        if (_itemList.isNotEmpty)
          setState(() {
            _itemList.removeAt(0);
          });
      }
      @override
      Widget build(BuildContext context) {
    
        return Scaffold(
          appBar: AppBar(
            title: Text('Key Demo'),
          ),
          body: Center(
            child: Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: _itemList,
            ),
          ),
          floatingActionButton: FloatingActionButton(
            onPressed: _deleteItem,
            tooltip: 'deleteItem',
            child: Icon(Icons.delete),
          ),
        );
      }
    }
    class Item extends StatefulWidget {
      final String title;
      Item(this.title);
      @override
      _ItemState createState() => _ItemState();
    }
    
    class _ItemState extends State<Item> {
      Color _color = Color.fromRGBO(Random().nextInt(256), Random().nextInt(256), Random().nextInt(256), 1);
      @override
      Widget build(BuildContext context) {
        return Container(
          color: _color,
          child: Center(
            child: Text(widget.title,style: TextStyle(color:  Colors.white,fontSize: 20,),),
          ),
        );
      }
    }
    

    但是结果却跟我们想要实现的功能不一样,如下图展示:


    Mar-23-2021 10-06-00.gif

    点击按钮确实按照A,B,C,D的顺序被删除了,但是颜色却是按着D,C,B,A的顺序被删了,为什么会出现这个奇怪的问题呢?

    我们来尝试解决这个问题。

    • 方案一
      Item这个WidgetStatefulWidget改为StatelessWidget
    class Item extends StatelessWidget {
      final String title;
      Item(this.title);
      final Color _color = Color.fromRGBO(Random().nextInt(256), Random().nextInt(256), Random().nextInt(256), 1);
    
      @override
      Widget build(BuildContext context) {
        return Container(
          color: _color,
          child: Center(
            child: Text(title,style: TextStyle(color:  Colors.white,fontSize: 20,),),
          ),
        );
      }
    }
    

    这次重现象上看是正常了,满足了需求:


    Mar-23-2021 10-38-10.gif
    • 方案二
      Item这个Widget还是StatefulWidget,之前_color是保存在State里面,现在修改为放在Widget中:
    class Item extends StatefulWidget {
      final String title;
    
      Item(this.title);
    
     final Color _color = Color.fromRGBO(
          Random().nextInt(256), Random().nextInt(256), Random().nextInt(256), 1);
    
      @override
      _ItemState createState() => _ItemState();
    }
    
    class _ItemState extends State<Item> {
      @override
      Widget build(BuildContext context) {
        return Container(
          color: widget._color,
          child: Center(
            child: Text(
              widget.title,
              style: TextStyle(
                color: Colors.white,
                fontSize: 20,
              ),
            ),
          ),
        );
      }
    }
    

    看一下结果:


    Mar-23-2021 10-48-39.gif

    也是满足需求的。

    来分析一下原因。

    首先要明确一点,Widget在重新build的时候,是增量更新的,而不是全部更新,那怎么实现增量更新的呢。Widget树生成的时候,Element树也同步生成,Widget会判断是否要更新Element的widget:

      /// 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;
      }
    

    这个方法的意义在于,Element会判断新旧Widget的runtimeType和key是否同时相等,如果相等就说明新旧的配置相同,如果相同就可以更新使用另一个小部件。

    • 先看看为什么最开始的代码不可以满足需求。
      第一次构建完页面,Widget树和Element树的关系如下:


      image.png

    Element树中的elementWidget树中的widget是一一对应的,element对象持有widgetstate两个对象,Itemtitle保存在widget中,而_color保存state中,如上图展示。当我们删除了第一个Item-AAAAAA之后,重新build时候这时候由于AAAAAA被删除了,BBBBBBIndex就变成了0Element树就会拿Index0widgetWidget树的index0widget比较,看看是否更新widget,比较的内容就是runtimeTypekey,由于AAAAAABBBBBB的类型都是Item,并且keynull,所以Element就认为widget没有发生变化,如实就更新了widget,但是state没有发生变化:

    image.png

    同理接着 build CCCCCC的时候Element会拿Index为1的element里面的widgetWidget树里Index1Element作比较,同样是可以更新的,如是就更新了widget但是state仍然没变:

    image.png

    同理 build DDDDDD

    image.png

    buildDDDDDD之后,Element发现有多的element没有widget与之对应,就把多的element对象干掉了:

    image.png

    上面就解释了为什么我们看到的是Title被正常删除了,但是颜色却是从后往前删的原因了。

    那为什么方案一方案二都可以解决这个问题呢,不管是方案一还是方案二都是将颜色也放到了elementwidget中了,这样在更新widget的时候,titlecolor就一起更新了。

    产生这个问题的根本原因是在更新的时候新旧widget的runtimeType和key都是一样的,runtimeType都是Item类型这个不会改变,那我们可以在创建每个Item时给它一个key,这样在比较widget的时候由于key不一样就不会更新而是重新生成新的element就可以解决问题了:

    List<Item> _itemList = [
        Item('AAAAAA',key: ValueKey(111111),),
        Item('BBBBBB',key: ValueKey(222222),),
        Item('CCCCCC',key: ValueKey(333333),),
        Item('DDDDDD',key: ValueKey(444444),),
      ];
      
      
    class Item extends StatefulWidget {
      final String title;
      Item(this.title,{Key key}):super(key:key);
      @override
      _ItemState createState() => _ItemState();
    }
    
    class _ItemState extends State<Item> {
      final Color _color = Color.fromRGBO(
          Random().nextInt(256), Random().nextInt(256), Random().nextInt(256), 1);
      @override
      Widget build(BuildContext context) {
        return Container(
          color: _color,
          child: Center(
            child: Text(
              widget.title,
              style: TextStyle(
                color: Colors.white,
                fontSize: 20,
              ),
            ),
          ),
        );
      }
    }
      
    

    添加一个key之后,widget里面就多了一个变量key,生成之后Widget树和Element树为:

    image.png

    当删除AAAAAA之后,重新构建BBBBBB的时候:

    image.png

    widget还是会取element的树index0widget来和自己比较看是否更新,这时key是不一致的,所以element就会被干掉重新生成了:

    image.png
    生成CCCCCCDDDDDD是同样的道理,这里就不展示了。这样就可以实现需求了。

    2、Key的类型及作用

    Key本身是一个虚类定义如下:

    @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 [new Key] factory
      /// constructor shadows the implicit constructor.
      @protected
      const Key.empty();
    }
    

    它的直接子类型有两个LocalKeyGlobalKey两种。

    2.1 LocalKey

    LocalKeydiff算法的核心所在,用做ElementWidget的比较。常用子类有以下几个:

    • ValueKey:以一个数据为作为key,比如数字、字符等。
    • ObjectKey:以Object对象作为Key
    • UniqueKey:可以保证key的唯一性,如果使用这个类型的key,那么Element对象将不会被复用。
    • PageStorageKey:用于存储页面滚动位置的key

    2.2 GlobalKey

    每个globalkey都是一个在整个应用内唯一的keyglobalkey相对而言是比较昂贵的,如果你并不需要globalkey的某些特性,那么可以考虑使用KeyValueKeyObjectKeyUniqueKey
    他有两个用途:

    • 用途1:允许widget在应用程序中的任何位置更改其parent而不丢失其状态。应用场景:在两个不同的屏幕上显示相同的widget,并保持状态相同。
    • 用途2:可以获取对应Widget的state对象:
    void main() {
      runApp(MyApp());
    }
    class MyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Flutter Demo',
          theme: ThemeData(
            primarySwatch: Colors.blue,
          ),
          home: MyHomePage(),
        );
      }
    }
    class MyHomePage extends StatelessWidget {
     final GlobalKey<_CounterState> _globalKey = GlobalKey();
      void _addCounter() {
        _globalKey.currentState.description = '旧值:'+ _globalKey.currentState.count.toString();
        _globalKey.currentState.count ++;
        _globalKey.currentState.setState(() {});
      }
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text('Key Demo'),
          ),
          body: Counter(key: _globalKey,),
          floatingActionButton: FloatingActionButton(
            onPressed: _addCounter,
            tooltip: 'deleteItem',
            child: Icon(Icons.add),
          ),
        );
      }
    }
    class Counter extends StatefulWidget {
      Counter({Key key}) : super(key: key);
      @override
      _CounterState createState() => _CounterState();
    }
    class _CounterState extends State<Counter> {
      String description = '旧值';
      int count = 0;
      @override
      Widget build(BuildContext context) {
        return Center(
          child: Column(children: [
            Text(
              '$count',
              style: TextStyle(
                color: Colors.black,
                fontSize: 20,
              ),
            ),
            Text(
              description,
              style: TextStyle(
                color: Colors.black,
                fontSize: 20,
              ),
            ),
          ]),
        );
      }
    }
    
    Mar-23-2021 16-52-34.gif

    这个例子比较简单,展示通过GlobalKey获取子WidgetState并更新。

    相关文章

      网友评论

        本文标题:二、Flutter中Key的作用

        本文链接:https://www.haomeiwen.com/subject/bawfhltx.html