How to use `GlobalKey` to maintain widgets' states when changing parents?

The most common use-case of using GlobalKey to move a widget around the tree is when conditionally wrapping a "child" into another widget like so:

Widget build(context) {
  if (foo) {
    return Foo(child: child);
  }
  return child;
}

With such code, you'll quickly notice that if child is stateful, toggling foo will make child lose its state, which is usually unexpected.

To solve this, we'd make our widget stateful, create a GlobalKey, and wrap child into a KeyedSubtree.

Here's an example:

class Example extends StatefulWidget {
  const Example({Key key, this.foo, this.child}) : super(key: key);

  final Widget child;
  final bool foo;

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

class _ExampleState extends State<Example> {
  final key = GlobalKey();

  @override
  Widget build(BuildContext context) {
    final child = KeyedSubtree(key: key, child: widget.child);

    if (widget.foo) {
      return Foo(child: child);
    }
    return child;
  }
}

I would not recommend using GlobalKey for this task.

You should pass the data around, not the widget, not the widget state. For example, if you want a Switch and a Slider like in the demo, you are better off just pass the actual boolean and double behind those two widgets. For more complex data, you should look into Provider, InheritedWidget or alike.

Things have changed since that video was released. Saed's answer (which I rewarded 50 bounty points) might be how it was done in the video, but it no longer works in recent Flutter versions. Basically right now there is no good way to easily implement the demo using GlobalKey.

But...

If you can guarantee that, the two widgets will never be on the screen at the same time, or more precisely, they will never be simultaneously inserted into the widget tree on the same frame, then you could try to use GlobalKey to have the same widget on different parts of the layout.

Note this is a very strict limitation. For example, when swiping to another screen, there is usually a transition animation where both screens are rendered at the same time. That is not okay. So for this demo, I inserted a "blank page" to prevent that when swiping.

Global key demo

How to:

So, if you want the same widget, appearing on very different screens (that hopefully are far from each other), you can use a GlobalKey to do that, with basically 3 lines of code.

First, declare a variable that you can access from both screens:

final _key = GlobalKey();

Then, in your widget, have a constructor that takes in a key and pass it to the parent class:

Foo(key) : super(key: key);

Lastly, whenever you use the widget, pass the same key variable to it:

return Container(
  color: Colors.green[100],
  child: Foo(_key),
);

Full Source:

import 'package:flutter/material.dart';

void main() {
  runApp(MaterialApp(home: MyApp()));
}

class MyApp extends StatelessWidget {
  final _key = GlobalKey();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("Global Key Demo")),
      body: PageView.builder(
        itemCount: 3,
        itemBuilder: (context, index) {
          switch (index) {
            case 0:
              return Container(
                color: Colors.green[100],
                child: Foo(_key),
              );
              break;
            case 1:
              return Container(
                color: Colors.blue[100],
                child: Text("Blank Page"),
              );
              break;
            case 2:
              return Container(
                color: Colors.red[100],
                child: Foo(_key),
              );
              break;
            default:
              throw "404";
          }
        },
      ),
    );
  }
}

class Foo extends StatefulWidget {
  @override
  _FooState createState() => _FooState();

  Foo(key) : super(key: key);
}

class _FooState extends State<Foo> {
  bool _switchValue = false;
  double _sliderValue = 0.5;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Switch(
          value: _switchValue,
          onChanged: (v) {
            setState(() => _switchValue = v);
          },
        ),
        Slider(
          value: _sliderValue,
          onChanged: (v) {
            setState(() => _sliderValue = v);
          },
        )
      ],
    );
  }
}

Update: this was an old approach to tackle the state management and not recommended anymore,please see my comments on this answer and also check user1032613's answer below

Global keys can be used to access the state of a statefull widget from anywhere in the widget tree

enter image description here

import 'package:flutter/material.dart';

main() {
  runApp(MaterialApp(
    theme: ThemeData(
      primarySwatch: Colors.indigo,
    ),
    home: App(),
  ));
}

class App extends StatefulWidget {
  @override
  State<App> createState() => _AppState();
}

class _AppState extends State<App> {
  GlobalKey<_CounterState> _counterState;

  @override
  void initState() {
    super.initState();
    _counterState = GlobalKey();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Center(
          child: Column(
        children: <Widget>[
          Counter(
            key: _counterState,
          ),
        ],
      )),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.navigate_next),
        onPressed: () {
          Navigator.of(context).push(
            MaterialPageRoute(builder: (context) {
              return Page1(_counterState);
            }),
          );
        },
      ),
    );
  }
}

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

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

class _CounterState extends State<Counter> {
  int count;

  @override
  void initState() {
    super.initState();
    count = 0;
  }

  @override
  Widget build(BuildContext context) {
    return Row(
      children: <Widget>[
        IconButton(
          icon: Icon(Icons.add),
          onPressed: () {
            setState(() {
              count++;
            });
          },
        ),
        Text(count.toString()),
      ],
    );
  }
}
class Page1 extends StatefulWidget {
  final GlobalKey<_CounterState> counterKey;
  Page1( this.counterKey);
  @override
  _Page1State createState() => _Page1State();
}

class _Page1State extends State<Page1> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Center(
        child: Row(
          children: <Widget>[
            IconButton(
              icon: Icon(Icons.add),
              onPressed: () {
                setState(() {
                  widget.counterKey.currentState.count++;
                  print(widget.counterKey.currentState.count);
                });
              },
            ),
            Text(
              widget.counterKey.currentState.count.toString(),
              style: TextStyle(fontSize: 50),
            ),
          ],
        ),
      ),
    );
  }
}

Tags:

Flutter