87

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);
  }
Ashton Thomas
  • 16,659
  • 5
  • 31
  • 25
  • 6
    You probably want to use `popAndPushNamed()` or similar instead of just `pushNamed()` to navigate between pages to avoid pages being added multiple times – Günter Zöchbauer Mar 19 '18 at 19:51
  • Thanks for the reply. Two things here 1) As luck would have it, I just started using fluro (https://github.com/goposse/fluro) and I don't yet know how `pop` work with that library.. 2) If this error is implying that `Screen A` is still around, is there a way I can just use that instance? (I think the error is all from `Screen A`. Perhaps because there is the original instance of `Screen A` and it's trying to create a new one – Ashton Thomas Mar 19 '18 at 19:57
  • 1
    That was my assumption as well. I don't know Fluro though. I think you need top pop the active route to go back to the previous one, or when you push a new route, replace the previous one instead of just adding one of top of the other. `popAndPushNamed()` is how the Flutter router does that, no idea about Fluro. – Günter Zöchbauer Mar 19 '18 at 20:04
  • 1
    @GunterZochbauer yep that was it! I didn't know Widget disposal was tied to 'popping' a route. I guess that makes sense, though. I stopped being lazy and looked at fluro's doc: https://github.com/goposse/fluro/blob/master/lib/src/router.dart#L43-L59 and setting `replace: true` triggered a call to `dispose()` so we are all good. Thanks! – Ashton Thomas Mar 19 '18 at 20:11
  • You could add a short Fluro code example as answer and accept it – Günter Zöchbauer Mar 19 '18 at 20:33

14 Answers14

136

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

flaugh
  • 1,517
  • 1
  • 10
  • 5
54

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>();
MindBrain
  • 7,398
  • 11
  • 54
  • 74
23

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)
Ashton Thomas
  • 16,659
  • 5
  • 31
  • 25
10

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

Youssef Ahmed
  • 162
  • 1
  • 5
6

Best way to solve that, which worked for me:

class _HomeScreenState extends State<HomeScreen> {
   GlobalKey<FormState> _homeKey = GlobalKey<FormState>(debugLabel: '_homeScreenkey');
   @override
   Widget build(BuildContext context) {
     return Container(
       key: _homeKey,
     );
   }
}
Dumitru Boaghi
  • 183
  • 2
  • 4
  • What is `debugLabel: '_homeScreenkey'`, Kindly explain that what actually you have did here. – mufazmi Jan 17 '22 at 05:37
  • `debugLabel` - it's used to define a individual `label`(a.k.a. name, id) for multiple GlobalKey in app. – Dumitru Boaghi Jan 27 '22 at 08:18
  • @DumitruBoaghi are you sure it is "a.k.a id"? Documentation states the opposite. `The label is purely for debugging and not used for comparing the identity of the key.` – mirkancal Feb 28 '23 at 03:51
6

I had this issue too. I had a four screen bottom tabbed application and a 'logout' method. However, that logout method was calling a pushReplacementNamed. This prevented the class that held the global keys (different from the logout function) from calling dispose.

The resolution was to change pushReplacementNamed with popAndPushNamed to get back to my 'login' screen.

Daniel Chapman
  • 173
  • 3
  • 7
  • I had a similar setup, but with the GetX package. Get.offAllNamed() was needed to route to and from the auth screen to ensure that all previous routes are disposed. https://github.com/jonataslaw/getx/blob/master/documentation/en_US/route_management.md#navigation-with-named-routes – King Leon Aug 12 '21 at 14:06
4

If you have multiple forms with different widgets, you must use separate GlobalKey for each form. Like I have two forms, one with Company signup & one with Profile. So, I declared

GlobalKey<FormState> signUpCompanyKey = GlobalKey<FormState>();
GlobalKey<FormState> signUpProfileKey = GlobalKey<FormState>();
Shihab Uddin
  • 6,699
  • 2
  • 59
  • 74
1

In my case I wanted to use the static GlobalKey<ScaffoldState> _scaffoldKey but when I used the same widget multiple times it gave this duplicate error.

I wanted to give it a unique string and still use this scaffold state. So I ended up using:

static GlobalObjectKey<ScaffoldState> _scaffoldKey

and in the initState:

_scaffoldKey = new GlobalObjectKey<ScaffoldState>(id);

Edit: Actually, silly me. I just simply removed the static and made it GlobalKey again :)

Ido
  • 2,034
  • 1
  • 17
  • 16
1

please take SingleChildScrollview:

and after if you use the bloc pettern then use strem with broadcast

code is here:

        body: Container(
        decoration: BoxDecoration(
          image: DecorationImage(
            image: AssetImage('assets/images/abcd.jpg'),
            fit: BoxFit.cover,
          ),
        ),
        child: Container(child:Form(key: _key,
          child: Padding(
            padding: EdgeInsets.symmetric(vertical: 100.0, horizontal: 20.0),
              child: SingleChildScrollView(child:Column(
                children: <Widget>[
                  Padding(
                    padding: const EdgeInsets.all(10.0),
                    child: Image.asset('assets/images/logo.png', height: 80, width:80,),
                  ),
                  email(),
                  Padding(
                    padding: EdgeInsets.all(5.0),
                  ),
                  password(),
                  row(context),
                ],
              ),
            ),
          ),
        ),
      ),
      ),
      resizeToAvoidBottomPadding: false,
    );
  }

and the bloc pettern code is here:

    final _email = StreamController<String>.broadcast();
    final _password = StreamController<String>.broadcast();

    Stream<String> get email => _email.stream.transform(validateEmail);
    Stream<String> get password=> _password.stream.transform(validatepassword);

    Function(String) get changeEmail=> _email.sink.add;
    Function(String) get changePassword => _password.sink.add;

    dispose(){

      _email.close();
      _password.close();
    }

}

final bloc=Bloc();
Vega
  • 27,856
  • 27
  • 95
  • 103
Harsh Chovatiya
  • 295
  • 2
  • 6
1

This happened to me, what I did was enclosed the whole view into a navigator using an extension I made

Widget addNavigator() => Navigator(
    onGenerateRoute: (_) => MaterialPageRoute(
      builder: (context2) => Builder(
        builder: (context) => this,
      ),
    ),
  );
Ketan Choyal
  • 93
  • 2
  • 7
1

I had similar issue on a StatelessWidget class, Converted it to StatefulWidget and error is gone.

Aziz
  • 461
  • 7
  • 18
0

I also got this error. There was a static bloc object in a class and I removed the static keyword which fixed the error.

Events should be added by using the BlocProvider anyway.

Colibri
  • 713
  • 12
  • 16
0

I was getting this error for using multiple loader in a single page for every API response delay. Applied above all solution but no luck.

finally, used separate Globalkey for every loader calling

GlobalKey<State> _loaderDialog = GlobalKey<State>(); 
GlobalKey<State> _loaderDialogForSubmit = GlobalKey<State>();

And solved my issue.

Mimu Saha Tishan
  • 2,402
  • 1
  • 21
  • 40
-1

I also had a similar error. My answer was that after I updated Flutter some widgets no longer had child or children properties. In my case it was the CircleAvatar. The build doesn't error out initially, but when navigating back and forth through the app it will fail.

*Please review all widgets that require a child then review the updated documentation and make sure you're parameters are still correct.

Charles Jr
  • 8,333
  • 15
  • 53
  • 74