What is Key


In Flutter we often deal with state. We know that there are two types of widgets, Stateful and Stateless, and Key helps developers to save state in the widget tree, but in general we don’t need to use Key, so when should we use Key.

 Let’s look at the following example.

class StatelessContainer extends StatelessWidget {
  final Color color = RandomColor().randomColor();
  
  @override
  Widget build(BuildContext context) {
    return Container(
      width: 100,
      height: 100,
      color: color,
    );
  }
}


This is a very simple Stateless Widget, which is a 100 * 100 Container with color. RandomColor can initialize a random color for this widget.

 We will now display this widget to the interface.

class Screen extends StatefulWidget {
  @override
  _ScreenState createState() => _ScreenState();
}

class _ScreenState extends State<Screen> {
  List<Widget> widgets = [
    StatelessContainer(),
    StatelessContainer(),
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: widgets,
        ),
      ),
      floatingActionButton: FloatingActionButton(
          onPressed: switchWidget,
        child: Icon(Icons.undo),
      ),
    );
  }

  switchWidget(){
    widgets.insert(0, widgets.removeAt(1));
    setState(() {});
  }
}


Here two StatelessContainer widgets are shown in the center of the screen. When we click on the floatingActionButton, the switchWidget is executed and their order is swapped.


It doesn’t look like there’s any problem, the swap operation is executed correctly. Now let’s make a small change to upgrade this StatelessContainer to a StatefulContainer.

class StatefulContainer extends StatefulWidget {
  StatefulContainer({Key key}) : super(key: key);
  @override
  _StatefulContainerState createState() => _StatefulContainerState();
}

class _StatefulContainerState extends State<StatefulContainer> {
  final Color color = RandomColor().randomColor();

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


In StatefulContainer, we put both the define Color and build methods into State.


Now we’re going to use the same layout as earlier, except we’re going to replace the StatelessContainer with a StatefulContainer and see what happens.


At this point, no matter how much we click, there is no way to swap the order of the two Containers anymore, and switchWidget does get executed.


To solve this problem, we pass a UniqueKey to both widgets when they are constructed.

class _ScreenState extends State<Screen> {
  List<Widget> widgets = [
    StatefulContainer(key: UniqueKey(),),
    StatefulContainer(key: UniqueKey(),),
  ];
  ···


Then the two widgets can be swapped again as normal.


You must be wondering why Stateful Widgets can’t swap order properly, but can after adding a Key, what’s going on here? In order to figure this out, we will cover the diff update mechanism of the widget.

 Widget Update Mechanism


In the previous post, we introduced the relationship between Widgets and Element. If you’re still confused about the concept of Element, read Flutter | BuildContext for a deeper understanding.

 Here’s a look at the Widget’s source code.

@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;
  }
}


We know that a widget is just a configuration and can’t be modified, whereas an element is an object that is actually used and can be modified.


The canUpdate method will be called when a new widget arrives to determine if the Element needs to be updated.


canUpdate compares the runtimeType and key of two (old and new) widgets to determine if the current Element needs to be updated. If the canUpdate method returns true, you don’t need to replace the Element, you can just update the Widget.

 StatelessContainer Comparison Process


In StatelessContainer, we don’t pass in a key, so we’re only comparing their runtimeType, where the runtimeType is the same, the canUpdate method returns true, the two widgets are swapped, and the StatelessElement calls the build method of the newly held widget to rebuild it, and our color is actually stored in the widget, so the two widgets are swapped correctly on the screen. StatelessElement calls the build method of the newly held widget to rebuild it, and our color is actually stored in the widget, so the two widgets are swapped in the correct order on the screen.

StatefulContainer


In the StatefulContainer example, we put the definition of the color in the State. The widget doesn’t hold the State, it’s the Stateful Element that actually holds the reference to the State.


When we don’t give any key to the widgets, only the runtimeType of the two widgets will be compared. Since both widgets have the same properties and methods, the canUpdate method will return true and update the position of the StatefulWidget, and the two Element will not swap positions. But the original Element will only build the new widget from the state instance it holds, because the element remains the same and the state it holds remains the same. So the colors won’t be swapped. Changing the position of the StatefulWidget has no effect here.


When we give the widget a key, the canUpdate method compares the runtimeType and key of the two widgets and returns false (in this case the runtimeType is the same, the key is different).


At this point, the RenderObjectElement will use the key of the new widget to look up the old Element list, and if it finds a match, it will update the position of the Element and update the position of the corresponding renderObject, which in this case means swapping the position of the Element and swapping the position of the corresponding renderObject. In this case, the position of the Element is swapped and the position of the corresponding renderObject is swapped.

 Thanks to ad6623 here for pointing out the previous mischaracterization.

 Scope of comparison


To improve performance Flutter’s comparison algorithm (diff) is scoped; it doesn’t compare the first StatefulWidget, but rather the widgets in a given hierarchy.

···
class _ScreenState extends State<Screen> {
  List<Widget> widgets = [
    Padding(
      padding: const EdgeInsets.all(8.0),
      child: StatefulContainer(key: UniqueKey(),),
    ),
    Padding(
      padding: const EdgeInsets.all(8.0),
      child: StatefulContainer(key: UniqueKey(),),
    ),
  ];
···


In this example, we wrap two keyed StatefulContainers with the Padding component and click on the Swap button, and the following wonderful thing happens.


Instead of swapping the order of the two widgets, the Element is recreated.


During Flutter’s comparison it goes down to the Row level and finds that it is a MultiChildRenderObjectWidget. It then scans all the children layers one by one.


At the Column level, the runtimeType of the padding has not changed and there is no key. canUpdate returns flase, and Flutter compares the next level. Since there is a key in the internal StatefulContainer, the old key is different from the new one, and the current level is inside the padding, there are no child widgets in that level. canUpdate returns a flase, and Flutter will think that this Element needs to be replaced. Flutter will think that the Element needs to be replaced, and then regenerate a new Element object to be loaded into the Element tree to replace the previous Element. the second Widget will do the same.


So to solve this problem, we need to put the key at the children level of the Row.

···
class _ScreenState extends State<Screen> {
  List<Widget> widgets = [
    Padding(
      key: UniqueKey(),
      padding: const EdgeInsets.all(8.0),
      child: StatefulContainer(),
    ),
    Padding(
      key: UniqueKey(),
      padding: const EdgeInsets.all(8.0),
      child: StatefulContainer(),
    ),
  ];
···


Now we can have fun again (swapping the order of widgets).

 Extended content


A slot describes the position of a child in its parent’s list. Multi-child widgets such as Row and Column provide a series of slots for their children.


There is a detail in the call to Element.updateChild, if the instances of the old and new widgets are the same, note that the instances are the same, not the types, and the slots are different, all Flutter does is update the slots, i.e., move them to a different location. Since widgets are immutable, having the same instance means that the configuration of the display is the same, so all you have to do is move it.

abstract class Element extends DiagnosticableTree implements BuildContext {
···
  dynamic get slot => _slot;
  dynamic _slot;
···
 @protected
  Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
    ···
    if (child != null) {
      if (child.widget == newWidget) {
        if (child.slot != newSlot)
          updateSlotForChild(child, newSlot);
        return child;
      }
      if (Widget.canUpdate(child.widget, newWidget)) {
        if (child.slot != newSlot)
          updateSlotForChild(child, newSlot);
        child.update(newWidget);
        assert(child.widget == newWidget);
        assert(() {
          child.owner._debugElementWasRebuilt(child);
          return true;
        }());
        return child;
      }
      deactivateChild(child);
      assert(child._parent == null);
    }
    return inflateWidget(newWidget, newSlot);
  }

  Updating the table of mechanisms

 New WIDGET is empty  New Widget not empty
 child is empty  Returns null.  Returns a new Element
 child is not null  Removes the old widget and returns null.
If the old child Element can be updated (canUpdate), it is updated and returned, otherwise a new Element is returned.

 Types of Keys

Key

@immutable
abstract class Key {
  const factory Key(String value) = ValueKey<String>;

  @protected
  const Key.empty();
}


The default Key creation will create a ValueKey based on the value passed in through the factory method.


Key derives from two different types of Key: LocalKey and GlobalKey.

Localkey


LocalKey inherits directly from Key and should be used when comparing widgets that have the same parent Element, i.e. in the above example there is a multi-child widget that needs to move its child widgets, this is where you should use Localkey.

 Localkey derives many subclasses of key:

  • ValueKey : ValueKey(‘String’)
  • ObjectKey : ObjectKey(Object)
  • UniqueKey : UniqueKey()

Valuekey   PageStorageKey : PageStorageKey(‘value’)

GlobalKey

@optionalTypeArgs
abstract class GlobalKey<T extends State<StatefulWidget>> extends Key {
···
static final Map<GlobalKey, Element> _registry = <GlobalKey, Element>{};
static final Set<Element> _debugIllFatedElements = HashSet<Element>();
static final Map<GlobalKey, Element> _debugReservations = <GlobalKey, Element>{};
···
BuildContext get currentContext ···
Widget get currentWidget ···
T get currentState ···


GlobalKey uses a static constant Map to hold its corresponding Element.


You can find the Widget, State and Element that holds a GlobalKey by using the GlobalKey.


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

 When to use Key

ValueKey


If you have a Todo List application, it will keep track of the things you need to accomplish. Let’s assume that each Todo is different, and you want to do a slide delete for each one.

 This is where ValueKey comes in!

return TodoItem(
    key: ValueKey(todo.task),
    todo: todo,
    onDismissed: (direction){
        _removeTodo(context, todo);
    },
);

ObjectKey


If you have a birthday app that keeps track of someone’s birthdays and displays them in a list, again there still needs to be a swipe to delete action.


We know that people’s names can be repeated, and you can’t guarantee that the value given to the Key will be different every time. However, when a person’s name and birthday are combined, the Object will be unique.

 This is when you need to use ObjectKey!

UniqueKey


If you want to make sure that each Key is unique when none of the combined Objects can satisfy uniqueness, then you can use UniqueKey. Then you can use UniqueKey, which will generate a unique hash code from the object.


But by doing this, every time the widget is built it goes and regenerates a new UniqueKey, losing consistency. Which means your widget will still change. (Might as well not use it 😂)

PageStorageKey


When you have a sliding list and you jump to a new page by a certain Item, and when you return to the previous list page, you find that the sliding distance is back to the top. At this point, give the Sliver a PageStorageKey! It will be able to keep the Sliver scrolling.

GlobalKey


GlobalKey can access state across widgets. Here we have a Switcher widget that can change its state via changeState.

class SwitcherScreenState extends State<SwitcherScreen> {
  bool isActive = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Switch.adaptive(
            value: isActive,
            onChanged: (bool currentStatus) {
              isActive = currentStatus;
              setState(() {});
            }),
      ),
    );
  }

  changeState() {
    isActive = !isActive;
    setState(() {});
  }
}


But we want to change that state externally, so we need to use GlobalKey.

class _ScreenState extends State<Screen> {
  final GlobalKey<SwitcherScreenState> key = GlobalKey<SwitcherScreenState>();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SwitcherScreen(
        key: key,
      ),
      floatingActionButton: FloatingActionButton(onPressed: () {
        key.currentState.changeState();
      }),
    );
  }
}


Here we define a GlobalKey and pass it to the SwitcherScreen, then we can use this key to get the SwitcherState it is bound to and call changeState externally to change the state.

By lzz

Leave a Reply

Your email address will not be published. Required fields are marked *