[fluent core class analysis] deeply understand Key

background

During the development of fluent, almost every Widget has an optional parameter - Key. However, we seldom pass this value. Since we don't need to pass it, what is the function of this Key?

problem

Let's take a look at this scenario first:

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      home: BodyWidget(),
    );
  }
}

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

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

class _BodyWidgetState extends State<BodyWidget> {
  List<Widget> list = [
    //The above two show StateLessColorBoxContainer
    StateLessColorBoxContainer(),
    StateLessColorBoxContainer(),
    //Split line
    Divider(
      height: 60.0,
      color: Colors.black,

    ),
		//The following two display StatefulColorBoxContainer
    StatefulColorBoxContainer(),
    StatefulColorBoxContainer(),
  ];

  void switchWidget() {
    setState(() {
      list.insert(0, list.removeAt(1));
      list.insert(3, list.removeAt(4));
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: new Text("KeyDemo"),
      ),
      body: Container(
        child: Center(
          child: Column(
            children: list,
          ),
        ),
      ),
      floatingActionButton: ElevatedButton(
          onPressed: () {
            switchWidget();
          },
          child: Text("Click exchange")),
    );
  }
}

class StateLessColorBoxContainer extends StatelessWidget {
  StateLessColorBoxContainer({Key key}) : super(key: key);

  final int random = Random().nextInt(4);
  final List<Color> colors = [
    Colors.red,
    Colors.yellow,
    Colors.green,
    Colors.grey
  ];

  @override
  Widget build(BuildContext context) {
    return Container(
      width: 100,
      height: 100,
      color: colors[random],
    );
  }
}

//Define StatefulColorBoxContainerContainer
class StatefulColorBoxContainer extends StatefulWidget {
  StatefulColorBoxContainer({Key key}) : super(key: key);

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

class _StatefulColorBoxContainerState extends State<StatefulColorBoxContainer> {
  int random = Random().nextInt(3);
  var colors = [Colors.red, Colors.yellow, Colors.green, Colors.grey];

  @override
  Widget build(BuildContext context) {
    return Container(
      width: 100,
      height: 100,
      color: colors[random],
    );
  }
}

The source code is easy to understand. The above two show two statelesscolorboxcontainers and the following two show StatefulColorBoxContainer. The build(context) methods of the two ColorBuildContainer classes are exactly the same. However, we find that when we click the exchange button below, only the above ones are exchanged each time, and the following ones are not exchanged no matter how many times they are clicked.

Why? Why can't you successfully exchange updates using StatefulWidget? This needs to be mentioned from the update mechanism of Widget.

Widget update mechanism

In the fluent framework, the view is maintained in the Tree structure. The widgets we write are nested one by one, and finally combined into a Tree.

StatelessWidget update mechanism

In the first implementation that uses statelesswidgets, when fluent renders these Widgets, the Row Widget provides an ordered set of slots for its child Widgets. For each Widget, fluent will build a corresponding Element. The constructed Element Tree is quite simple. It only saves information about each Widget type and references to child Widgets. You can use this Element Tree as the skeleton of your fluent App. It shows the structure of the App, but other information needs to be found by referencing the original Widget.

When we swap two color blocks in a row, fluent traverses the Widget tree to see if the skeleton structure is the same. It starts with the Row Widget, and then moves to its child Widget. The Element tree checks whether the Widget is of the same type and Key as the old Widget. If all are the same, it updates the reference to the new Widget. Here, the Widget does not have a Key set, so the fluent only checks the type. It did the same thing to the second child. Therefore, the Element tree will be updated according to the Widget tree.

When the Element Tree is updated, fluent will build a Render Object Tree based on the Element Tree and finally start the rendering process.

StatefulWidget update mechanism

When implemented with StatefulWidget, the structure of the control tree is similar, but now the color information is not stored in the control itself, but in the external State object.

Now, we click the button to exchange the order of the controls. Fluent will traverse the Element tree, check the Row control in the Widget tree and update the reference in the Element tree. Then, the first StatefulColorBoxContainer control checks whether its corresponding control is of the same type, and it finds that the opposite is of the same type; Then the second StatefulColorBoxContainer control does the same thing, resulting in Flutter thinking that neither of the two controls has changed. Fluent uses the Element tree and the State of its corresponding control to determine the content to be displayed on the device, so the Element tree does not change, and the displayed content will not change.

So how to solve the problem that StatefulWidget is not updated? This requires Key

StatefullWidget combined with Key

There is a method canUpdate in StatefullWidget. Let's look at the source code first:

@immutable
abstract class Widget extends DiagnosticableTree {
  const Widget({ this.key });
  final Key key;
  ยทยทยท
  static bool canUpdate(Widget oldWidget, Widget newWidget) {
    return oldWidget.runtimeType == newWidget.runtimeType
        && oldWidget.key == newWidget.key;
  }
}

It is not difficult to judge from the source code that in the above example, all canUpdate returns true, which means that he does not go back to re create the Element, but updates the Element through the widget configuration information. However, the StatefulColorBoxContainer above does not save the color information, so the Element will not be updated. If we want to correctly perform the update operation with the same runtimeType, we can only use the key. If the key of each widget is different, that is, canUpdate returns false, its Element will be rebuilt and the exchange can be performed correctly.

In the above example, we can exchange correctly by adding the key parameter to the StatefulColorBoxContainer.

  List<Widget> list = [
    StateLessColorBoxContainer(),
    StateLessColorBoxContainer(),
    //Split line
    Divider(
      height: 60.0,
      color: Colors.black,
    ),
		// Add different key s
    StatefulColorBoxContainer(key: UniqueKey()),
    StatefulColorBoxContainer(key: UniqueKey()),
  ];

Type of Key

The purpose of Key is to indicate a unique identity for each Widget. The Key to use depends on the specific use scenario.

ValueKey

For example, in a ToDo list application, the text of each Todo Item is constant and unique. In this case, ValueKey is suitable, and value is text.

ObjectKey

Suppose that each sub Widget stores a more complex combination of data, such as an address book application of user information. Any single field, such as name or birthday, may be the same as another entry, but each data combination is unique. In this case, ObjectKey is the most appropriate.

UniqueKey

If there are multiple widgets with the same value in the collection, or if you want to ensure that each Widget is different from other widgets, you can use UniqueKey. In our example, UniqueKey is used because we don't store any other constant data on our color block, and we don't know what color is before building the Widget.

Do not use random numbers in the Key. If you set it like that, a new random number will be generated every time the Widget is built, and the Element tree will not be updated consistently with the Widget tree.

GlobalKeys

Global Keys serves two purposes.

They allow widgets to change the parent anywhere in the application without losing the State, or they can be used to access information about another Widget in a completely different part of the Widget tree.

For example, to display the same Widget on two different screens while maintaining the same State, you need to use GlobalKeys.

In the second case, you may want to verify the password but do not want to share the status information with other widgets in the tree. You can use GlobalKey to hold the State of a Form.

summary

How to use Key properly:

  1. When: use Key when you want to keep the state of the Widget tree. For example, when modifying the Widget collection of the same type (such as in the list)
  2. Where: set the Key at the top of the Widget tree to indicate the unique identity
  3. Which: select different types of keys to use according to the data types stored in the Widget

Tags: Flutter

Posted on Tue, 09 Nov 2021 12:55:20 -0500 by teamatomic