1. Introduction

There is an optional parameter key in the constructor of many classes that I have seen before, but I ignored it because I did not use it very much in writing the code. Later, WHEN I encountered some problems, I found that this small key was very useful, so I decided to study it carefully. Read some articles are analysis source code, written unclear, obscure, so I decided to pass two practical small demo to tell. Before reading this article, it is best to have a look at Flutter Rendering Widgets, Elements and RenderObjects as a foundation.

Demo1 — Delete the first cell in the list

demand

A list contains multiple lines of cells. Click the button to delete the first cell each time.

The implementation process

This requirement can be said to be very simple, not to say much, first make a table and button out, according to previous programming experience, delete the first element in the data source, and then reloadData can be a piece of cake.

class KeyDemo2Page extends StatefulWidget {
  @override
  _KeyDemo2PageState createState() => _KeyDemo2PageState();
}

class _KeyDemo2PageState extends State<KeyDemo2Page> {
  final List<String> _names = ["1"."2"."3"."4"."5"."6"."Seven"];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Colors.blue,
        centerTitle: true,
        title: Text("Key demo"),
      ),
      body: ListView(
        children: _names.map((item) {
          return KeyItemLessWidget(item);
        }).toList(),
      ),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.delete),
        onPressed: (){
          setState(() {
            _names.removeAt(0); }); },),); }}class KeyItemLessWidget extends StatelessWidget {
  final String name;
  final randColor = Color.fromARGB(255, Random().nextInt(256), Random().nextInt(256), Random().nextInt(256));
  KeyItemLessWidget(this.name);

  @override
  Widget build(BuildContext context) {
    return Container(
      child: Text(name, style: TextStyle(color: Colors.white, fontSize: 50),),
      height: 80, color: randColor, ); }}Copy the code

Our cell uses the StatelessWidget and creates a random color inside it. The resulting interface looks like this.

image.png

The interface is exactly as we imagined. When we click the button, we find that the result is completely different from what we imagined. Every time we click the button, the number of cells does decrease, but the color of the cells is different from before.

This is easy to understand if YOU read my previous article “Flutter Rendering Widgets, Elements and RenderObjects”. SetState is executed when the button is clicked, and the build method is re-executed, including for each cell. The code to generate a random color for each cell is in build, so every time you click the button here, the cell color will be changed again.

Therefore, to solve the problem that the cell color changes every time you click a button, you need to change the cell from a StatelessWidget to a StatefullWidget. Save the color in State, so it should not change. The cell code is as follows.

class KeyItemLessWidget extends StatefulWidget {
  final String name;

  KeyItemLessWidget(this.name);

  @override
  _KeyItemLessWidgetState createState() => _KeyItemLessWidgetState();
}

class _KeyItemLessWidgetState extends State<KeyItemLessWidget> {
  final randColor = Color.fromARGB(255, Random().nextInt(256), Random().nextInt(256), Random().nextInt(256));

  @override
  Widget build(BuildContext context) {
    return Container(
      child: Text(widget.name, style: TextStyle(color: Colors.white, fontSize: 50),),
      height: 80, color: randColor, ); }}Copy the code

As expected, the color of the cell did remain and the number of cells was reduced by one. When we click the button, we want to delete the first cell, which is the first data, but we actually delete the number from the head each time, but the color on the screen is removed from the tail. That’s weird. It still doesn’t meet our needs.

You can also understand this phenomenon by looking at what Element does in the previous article. When a cell is created, it creates the corresponding Element, which contains references to the Widget and State, where the color is stored. Each time the button is clicked, setState is executed, and the cell is recreated, using the canUpdate method to determine whether the Element should be rebuilt or replaced directly with the 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;
  }
Copy the code

Through the source can be seen by comparing the type and key to determine. We have no key here, the same type, and we can see from the comment that this case returns true. So just replace the old widgets in Element with new ones. However, the State in Element is still there, and our colors are still there. The Widget we create uses the same State and uses the build method in that State, so the color in the header is not removed. The trailing Element realizes that the other elements have Widget updates and that it is unnecessary, so the Flutter will therefore unmount it. So what we see is that the number is the head of the delete, but the color is the tail of the delete.

To solve the problem of removing tail colors due to Element reuse, we need to make canUpdate return false so that each Element is recreated. So here’s the Key.

class KeyDemo2Page extends StatefulWidget {
  @override
  _KeyDemo2PageState createState() => _KeyDemo2PageState();
}

class _KeyDemo2PageState extends State<KeyDemo2Page> {
  final List<String> _names = ["1"."2"."3"."4"."5"."6"."Seven"];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Colors.blue,
        centerTitle: true,
        title: Text("Key demo"),
      ),
      body: ListView(
        children: _names.map((item) {
          return KeyItemLessWidget(item, key: Key(item),);
        }).toList(),
      ),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.delete),
        onPressed: (){
          setState(() {
            _names.removeAt(0); }); },),); }}class KeyItemLessWidget extends StatefulWidget {
  final String name;

  KeyItemLessWidget(this.name, {Key key}): super(key: key);

  @override
  _KeyItemLessWidgetState createState() => _KeyItemLessWidgetState();
}

class _KeyItemLessWidgetState extends State<KeyItemLessWidget> {
  final randColor = Color.fromARGB(255, Random().nextInt(256), Random().nextInt(256), Random().nextInt(256));

  @override
  Widget build(BuildContext context) {
    return Container(
      child: Text(widget.name, style: TextStyle(color: Colors.white, fontSize: 50),),
      height: 80, color: randColor, ); }}Copy the code

Notice a couple of things here

  1. A constructor is added to the KeyItemLessWidget
KeyItemLessWidget(this.name, {Key key}): super(key: key);
Copy the code
  1. The key is passed in when the object is created
return KeyItemLessWidget(item, key: Key(item),);
Copy the code

So far, the final requirements have been realized, which is very different from native development, and the process is worth understanding. In the end, the key is to rely on key to solve the problem.

3. Demo2 — Swap two widgets

demand

Click the button to switch the location of the two widgets.

The implementation process

Implementation idea, exchange data source data location, execute setState, the code is as follows

class KeyDemo1Page extends StatefulWidget {
  @override
  _KeyDemo1PageState createState() => _KeyDemo1PageState();
}

class _KeyDemo1PageState extends State<KeyDemo1Page> {

  List<Widget> tiles = [
    StatelessColorfulTile(),
    StatelessColorfulTile(),
  ];

  Widget _itemForRow(BuildContext context, int index) {
    return tiles[index];
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Colors.blue,
        centerTitle: true,
        title: Text("Key demo"),
      ),
      body: Column(children: tiles,),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.delete),
        onPressed: (){
          setState(() {
            tiles.insert(1, tiles.removeAt(0)); }); },),); }}class StatelessColorfulTile extends StatelessWidget {
  Color myColor = Color.fromARGB(255, Random().nextInt(256), Random().nextInt(256), Random().nextInt(256));

  @override
  Widget build(BuildContext context) {
    return Container(
        color: myColor,
        child: Padding(padding: EdgeInsets.all(70.0))); }}Copy the code

The interface is as follows

Here we use the StatelessWidget, and the result is exactly what we want. Click the button and the two widgets successfully switch places. Let’s make a little change here and change the square to a StatefulWidget. Here’s the code.

class StatelessColorfulTile extends StatefulWidget {
  @override
  _StatelessColorfulTileState createState() => _StatelessColorfulTileState();
}

class _StatelessColorfulTileState extends State<StatelessColorfulTile> {
  Color myColor = Color.fromARGB(255, Random().nextInt(256), Random().nextInt(256), Random().nextInt(256));

  @override
  Widget build(BuildContext context) {
    return Container(
        color: myColor,
        child: Padding(padding: EdgeInsets.all(70.0))); }}Copy the code

The result is no response when the button is clicked. What’s going on here?

Again, the State of the StatefulWidget is inside Element. Although the Widget does change its location, Element returns true through the canUpdate method. So Element simply replaces its Element reference. Re-execute the build method of the previous State, and the color is the same, so it looks like nothing has changed.

For switching purposes, this is the time to use Key again. The complete code is as follows.

class KeyDemo1Page extends StatefulWidget {
  @override
  _KeyDemo1PageState createState() => _KeyDemo1PageState();
}

class _KeyDemo1PageState extends State<KeyDemo1Page> {
  List<StatelessColorfulTile> tiles = [
    StatelessColorfulTile(key: UniqueKey(),),
    StatelessColorfulTile(key: UniqueKey(),),
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Colors.blue,
        centerTitle: true,
        title: Text("Key demo"),),// body: ListView(
// children:tiles,
/ /),
      body: Column(children: tiles,),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.delete),
        onPressed: (){
          setState(() {
            tiles.insert(1, tiles.removeAt(0)); }); },),); }}class StatelessColorfulTile extends StatefulWidget {
  StatelessColorfulTile({Key key}): super(key: key);

  @override
  _StatelessColorfulTileState createState() => _StatelessColorfulTileState();
}

class _StatelessColorfulTileState extends State<StatelessColorfulTile> {
  Color myColor = Color.fromARGB(255, Random().nextInt(256), Random().nextInt(256), Random().nextInt(256));

  @override
  Widget build(BuildContext context) {
    return Container(
        color: myColor,
        child: Padding(padding: EdgeInsets.all(70.0))); }}Copy the code

UniqueKey() is used here, which will be explained later. Results successfully switch, to achieve the requirements we proposed.

4. Key classification

The key integration is shown in the figure above. Key itself is an abstract class. Key is defined as follows

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();
}
Copy the code

It has a factory constructor that creates a ValueKey. Key direct subclasses include LocalKey and GlobalKey.

4.1 LocakKey

It is used to compare widgets that have the same parent Element and is at the heart of the DIff algorithm. It has three subclasses: ValueKey, ObjectKey, and UniqueKey.

• ValueKey: used when we use a specific value as a key, such as a string, number, etc. • ObjectKey: If two students have the same name and do not use name as their key, it is not appropriate to create a student object and use the object as the key. • UniqueKey: We want to ensure that the key is unique and can use UniqueKey. If you want to force a refresh so that Element is recreated each time, you can use UniqueKey.

4.2 GlobalKey

GlobalKey uses a static constant Map to hold its Element, and you can find the Widget, State, and Element that holds the GlobalKey. An automatic message is displayed in the following figure.

For example, in Demo1, we can use GlobalKey to print the name property of each cell when clicking the button. The complete code is as follows

class KeyDemo2Page extends StatefulWidget {
  @override
  _KeyDemo2PageState createState() => _KeyDemo2PageState();
}

class _KeyDemo2PageState extends State<KeyDemo2Page> {
  final List<String> _names = ["1"."2"."3"."4"."5"."6"."Seven"];
  final GlobalKey<_KeyItemLessWidgetState> globalKeyTest = GlobalKey();
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Colors.blue,
        centerTitle: true,
        title: Text("Key demo"),
      ),
      body: KeyItemLessWidget(Hello Trip, key: globalKeyTest,),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.delete),
        onPressed: (){
          print(globalKeyTest.currentState.widget.name); },),); }}class KeyItemLessWidget extends StatefulWidget {
  final String name;
  KeyItemLessWidget(this.name, {Key key}): super(key: key);

  @override
  _KeyItemLessWidgetState createState() => _KeyItemLessWidgetState();
}

class _KeyItemLessWidgetState extends State<KeyItemLessWidget> {
  final randColor = Color.fromARGB(255, Random().nextInt(256), Random().nextInt(256), Random().nextInt(256));

  @override
  Widget build(BuildContext context) {
    return Container(
      child: Text(widget.name, style: TextStyle(color: Colors.white, fontSize: 50),),
      height: 80, color: randColor, ); }}Copy the code

Note: GlobalKey is very expensive and should be used with caution.

5. To summarize

This article introduces key through two simple demo, you can understand the important role of key. For more information, please refer to the previous article “Flutter Rendering Widgets, Elements and RenderObjects”. The first step is to understand the relationship between widgets, Elements, and RenderObjects. After realizing the role of Key, I will classify Key from the code level to facilitate the system to master it. Finally, I will give a small demo to understand the role of GlobalKey. In general, this article does not speak too much source code related content, try to explain step by step through the actual requirements, it is easy to understand.

There is a problem in demo2- swapping two widgets. Another article was written to look at a neglected aspect of the ListView in Flutter