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.
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
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),
),
],
),
),
);
}
}