在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,),),
      ),
    );
  }
}
但是结果却跟我们想要实现的功能不一样,如下图展示:

点击按钮确实按照A,B,C,D的顺序被删除了,但是颜色却是按着D,C,B,A的顺序被删了,为什么会出现这个奇怪的问题呢?
我们来尝试解决这个问题。
- 方案一
将Item这个Widget有StatefulWidget改为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,),),
      ),
    );
  }
}
这次重现象上看是正常了,满足了需求:

- 方案二
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,
          ),
        ),
      ),
    );
  }
}
看一下结果:

也是满足需求的。
来分析一下原因。
首先要明确一点,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树中的element与Widget树中的widget是一一对应的,element对象持有widget和state两个对象,Item的title保存在widget中,而_color保存state中,如上图展示。当我们删除了第一个Item-AAAAAA之后,重新build时候这时候由于AAAAAA被删除了,BBBBBB的Index就变成了0,Element树就会拿Index为0的widget与Widget树的index为0的widget比较,看看是否更新widget,比较的内容就是runtimeType和key,由于AAAAAA和BBBBBB的类型都是Item,并且key为null,所以Element就认为widget没有发生变化,如实就更新了widget,但是state没有发生变化:

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

同理 build DDDDDD:

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

上面就解释了为什么我们看到的是Title被正常删除了,但是颜色却是从后往前删的原因了。
那为什么方案一和方案二都可以解决这个问题呢,不管是方案一还是方案二都是将颜色也放到了element的widget中了,这样在更新widget的时候,title和color就一起更新了。
产生这个问题的根本原因是在更新的时候新旧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树为:

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

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

生成
CCCCCC、DDDDDD是同样的道理,这里就不展示了。这样就可以实现需求了。
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();
}
它的直接子类型有两个LocalKey和GlobalKey两种。
2.1 LocalKey
LocalKey是diff算法的核心所在,用做Element和Widget的比较。常用子类有以下几个:
- 
ValueKey:以一个数据为作为key,比如数字、字符等。 - 
ObjectKey:以Object对象作为Key。 - 
UniqueKey:可以保证key的唯一性,如果使用这个类型的key,那么Element对象将不会被复用。 - 
PageStorageKey:用于存储页面滚动位置的key。 
2.2 GlobalKey
每个globalkey都是一个在整个应用内唯一的key。globalkey相对而言是比较昂贵的,如果你并不需要globalkey的某些特性,那么可以考虑使用Key、ValueKey、ObjectKey或UniqueKey。
他有两个用途:
- 用途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,
          ),
        ),
      ]),
    );
  }
}

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