Duplicate GlobalKey detected in widget tree

I am running into a globalKey error after I navigate from Screen A to Screen B and click a "Cancel" button to go back to Screen A.

It seems like the issue is that Screen B is either

  • A) Not being disposed of correctly
  • B) Is not doing something that it otherwise could

And I don't actually know:

  • What bad things are happening if I just remove the use of a globalKey? (as to get a better understanding of the fundamentals)
  • How can I correctly resolve this issue?

StatefulWidget documentation states:enter link description here

A StatefulWidget keeps the same State object when moving from one location in the tree to another if its creator used a GlobalKey for its key. Because a widget with a GlobalKey can be used in at most one location in the tree, a widget that uses a GlobalKey has at most one associated element. The framework takes advantage of this property when moving a widget with a global key from one location in the tree to another by grafting the (unique) subtree associated with that widget from the old location to the new location (instead of recreating the subtree at the new location). The State objects associated with StatefulWidget are grafted along with the rest of the subtree, which means the State object is reused (instead of being recreated) in the new location. However, in order to be eligible for grafting, the widget must be inserted into the new location in the same animation frame in which it was removed from the old location.

Console Error Output:

══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞═══════════════════════════════════════════════════════════
The following assertion was thrown while finalizing the widget tree:
Duplicate GlobalKey detected in widget tree.
The following GlobalKey was specified multiple times in the widget tree. This will lead to parts of
the widget tree being truncated unexpectedly, because the second time a key is seen, the previous
instance is moved to the new location. The key was:
- [LabeledGlobalKey<FormFieldState<String>>#3c76d]
This was determined by noticing that after the widget with the above global key was moved out of its
previous parent, that previous parent never updated during this frame, meaning that it either did
not update at all or updated before the widget was moved, in either case implying that it still
thinks that it should have a child with that global key.
The specific parent that did not update after having one or more children forcibly removed due to
GlobalKey reparenting is:
- Column(direction: vertical, mainAxisAlignment: start, crossAxisAlignment: center, renderObject:
RenderFlex#7595c relayoutBoundary=up1 NEEDS-PAINT)
A GlobalKey can only be specified on one widget at a time in the widget tree.

So this part of the error output:

previous parent never updated during this frame, meaning that it either did not update at all or updated before the widget was moved

makes me think there was some opportunity for my old Stateful widget to do something (either reposition itself or release something as to be disposed correctly.

This seems to be failing in framework.dart on assert(_children.contains(child)):

  @override
  void forgetChild(Element child) {
    assert(_children.contains(child));
    assert(!_forgottenChildren.contains(child));
    _forgottenChildren.add(child);
  }

Solution 1:

In my case, it likes a hot reload bug. Just restart debugging works for me.

Solution 2:

Remove the static and final type from the key variable so if

static final GlobalKey<FormState> _abcKey = GlobalKey<FormState>();

change it to

GlobalKey<FormState> _abcKey = GlobalKey<FormState>();

Solution 3:

Thanks to Gunter's commments, I determined that this is because the Screens are not being properly disposed.

Flutter's pushReplacement makes a call to Route.dispose which will ultimately dispose the screen.

I am still unsure as to this comes into play:

widget must be inserted into the new location in the same animation frame

I'm not sure what situation would benefit from such trickery. However, my problem is solved. I just need to make a call to pop or replace.

Here are the available options:

  • Use push from A to B and just Navigator.pop from B
  • Use pushReplacement from A to B and from B to A

I've recently started playing with Fluro for routing and there are a few more ways to to handle these situations (Note the optional argument replace):

  • Use router.navigateTo(context, route, replace: false) from A to B and Navigator.pop from B
  • Use router.navigateTo(context, route, replace: true) from A to B the same from B to A (the key is replace: true)

Solution 4:

Make sure that you don't have a Form parent and a Form child with the same key