Those of you who have worked with Flutter will no doubt be familiar with Key. We can see that there is a named parameter Key in the constructor of Flutter in all widgets, such as the StatefulWidget:

abstract class StatefulWidget extends Widget{
/// Initializes [key] for subclasses.
 const StatefulWidget({ Key? key }) : super(key: key); . }Copy the code

So I’m sure you’re asking what this key is for? Why put a seemingly useless parameter here? Today we’re going to find out.

Here is an 🌰

Let’s take a closer look at the following example. Close the door and put the code 😏

// Create a page with three color blocks in the middle, and remove the first element of the _containers array with each click of the button
// Please observe the change of color block carefully
class _MyHomePageState extends State<MyHomePage> {

  final List<AAContainer> _containers = [
    const AAContainer('A'),
    const AAContainer('B'),
    const AAContainer('C')];void _updateColor(){

    _containers.removeAt(0);
    setState(() {});
  }

  @override
  Widget build(BuildContext context) {

    return Scaffold(
      appBar: AppBar(
        title: const Text('key demo'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: _containers
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _updateColor,
        child: const Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.); }}Copy the code

The AAContainer code is as follows:

class AAContainer extends StatefulWidget {

  final String title;
  const AAContainer(this.title,{Key? key}) : super(key: key);
  @override
  _AAContainerState createState() => _AAContainerState();
}

class _AAContainerState extends State<AAContainer> {

  final Color _color = Color.fromRGBO(Random().nextInt(256),Random().nextInt(256),Random().nextInt(256),1);

  @override
  Widget build(BuildContext context) {
    return Container(
      height: 100,
      width: 100, color: _color, child: Center( child: Text(widget.title), ), ); }}Copy the code

The running results are as follows:

Wait, let’s agree on what we know about these colors:

  • Block A: red
  • B Color block: blue
  • Block C: purple

The expected result of the code above is that the red disappears on the first click, the blue disappears on the second click, and the purple disappears on the third click. That would be perfect, but is it true? See the GIF below

Aye? What’s up… Why don’t we do it the way we always do it? To understand this, we need to investigate how Flutter can achieve page refresh

Page Refresh Principle

Flutter is known to refresh the page with incremental updates. How does Flutter determine which part of the page has changed?

setState

We know that in order for the StatefulWidget to refresh, we need to call the setState method, so what’s going on in this method? Let’s break through from here. SetState source

@protected
  void setState(VoidCallback fn) {
    // Extraneous code has been omitted_element! .markNeedsBuild(); }Copy the code

The only method called in setState is markNeedsBuild, so we go to markNeedsBuild:

markNeedsBuild
void markNeedsBuild() {
    // Extraneous code has been omitted
    if(_lifecycleState ! = _ElementLifecycle.active)return;
    if (dirty)
      return;
    _dirty = true;
    // Owner is the BuildOwner instance object, and this is the current elementowner! .scheduleBuildFor(this);
  }
Copy the code

MarkNeedsBuild marks the current element as dirty and adds this (the current element) to the render queue. Next we use the KangkangScheduleBuildFor method:

scheduleBuildFor
void scheduleBuildFor(Element element) {
    // The assertion code has been omitted
    if(! _scheduledFlushDirtyElements && onBuildScheduled ! =null) {
      _scheduledFlushDirtyElements = true; onBuildScheduled! (a); } _dirtyElements.add(element); element._inDirtyList =true;
  }
Copy the code

This method simply adds the current element to the render queue and marks it as already in the queue. At this point we have no idea, is this the end of it? By looking at the source, we found that there is another way to BuildOwner buildScope, through debugging, we found that each call setState would come here, we’ll kangkang this part of the source code:

buildScope
void buildScope(Element context, [ VoidCallback? callback ]) {

  _dirtyElements.sort(Element._sort);
  _dirtyElementsNeedsResorting = false;
  int dirtyCount = _dirtyElements.length;
  int index = 0;
  while(index < dirtyCount) { _dirtyElements[index].rebuild(); }}Copy the code

The scheduleBuildFor element is added to _dirtyElements and the rebuild method is called.

rebuild
  void rebuild() {
    // The assertion code has been omitted
    performRebuild();
  }
Copy the code

The element corresponding to the StatefulWidget is a StatefulElement, which in turn inherits from ComponentElement, so look directly at the performRebuild method for ComponentElement:

performRebuild
void performRebuild() {
    // Omit a lot of judgment code
    built = build();
    _child = updateChild(_child, built, slot);
}
Copy the code

😤 is so looking for go down very tired? Ha ha, don’t worry, it will be over soon. Next we move to the updateChild method:

updateChild
Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
    // If the current widget is deleted in widgetTree, it ends up being deleted in ElementTree as well
    if (newWidget == null) {
        deactivateChild(child);
        return; }...Element newChild;
    if(child ! =null) {...if (hasSameSuperclass && child.widget == newWidget) {
        // If they are the same, no updata operation is performed
             newChild = child;
        } else if (hasSameSuperclass && Widget.canUpdate(child.widget, newWidget)) {
        // Update if it can be updated
             child.update(newWidget);
             newChild = child;
        }else{ newChild = inflateWidget(newWidget, newSlot); }}}Copy the code

Update (canUpdate); update (canUpdate);

canUpdate
static bool canUpdate(Widget oldWidget, Widget newWidget) {
    return oldWidget.runtimeType == newWidget.runtimeType
        && oldWidget.key == newWidget.key;
}
Copy the code

AAContainer’s runtimeType is the same as its key, so it calls element’s update method. But the update method simply points the current Element’s widget to the new widget, meaning that the red Element points to the blue widget

update
void update(covariant Widget newWidget) {
    _widget = newWidget;
}
Copy the code

This means that after the update, the red Element is still in the Element tree, but the red Element. widget points to the blue widget, which causes the error in the demo above.

So how can this problem be solved?

The solution

From the above analysis, we can conclude that the result is wrong because the update method is called. Is there any way to prevent Element from calling update? There must be, and that’s the subject of today’s discussion. Let’s make a transformation to 🌰 :

final List<AAContainer> _containers = [
    const AAContainer('A',key: ValueKey('A'),),
    const AAContainer('B',key: ValueKey('B'),),
    const AAContainer('C',key: ValueKey('C'),)];Copy the code

Create AAContainer (key);

How is it, isn’t it perfect 😁😁😁

The classification of the Key

Now that we know how keys work, let’s take a look at the categories of keys:

LocalKey

LocalKey is the heart of the Diff algorithm and is used to compare Elements and widgets. The following keys also inherit from LocalKey:

  • ValueKey: Use any type as the key
  • ObjectKey: Uses an Object as the Key
  • UniqueKey: Ensures that the Key is unique. Once UniqueKey is used, there is no element reuse
GlobalKey

GlobalKey retrieves the State object of the Widget. To learn how to use GlobalKey, see 🌰 :

class GlobalKeyDemo extends StatelessWidget {
  final GlobalKey<_ChildPageState> _globalKey = GlobalKey();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('GlobalKeyDemo'),
      ),
      body: ChildPage(
        key: _globalKey,
      ),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.add),
        onPressed: () {
          _globalKey.currentState.data =
              'old:'+ _globalKey.currentState.count.toString(); _globalKey.currentState.count++; _globalKey.currentState.setState(() {}); },),); }}class ChildPage extends StatefulWidget {
  ChildPage({Key key}) : super(key: key);
  @override
  _ChildPageState createState() => _ChildPageState();
}

class _ChildPageState extends State<ChildPage> {
  int count = 0;
  String data = 'hello';
  @override
  Widget build(BuildContext context) {
    returnCenter( child: Column( children: <Widget>[ Text(count.toString()), Text(data), ], ), ); }}Copy the code

In 🌰, we updated the count value of the child widget in the parent widget.

Goodbye, over!