flutterFlutterflutter

二、Flutter中Key的作用

2021-03-23  本文已影响0人  Mlqq

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的顺序被删了,为什么会出现这个奇怪的问题呢?

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

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
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是否同时相等,如果相等就说明新旧的配置相同,如果相同就可以更新使用另一个小部件。

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的比较。常用子类有以下几个:

2.2 GlobalKey

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

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并更新。

上一篇下一篇

猜你喜欢

热点阅读