We have previously used many components, such as Container
, Row
, or ElevatedButton
widgets, which are used quite frequently. However, I wonder if you have noticed that their constructors always have a Key as a property, which usually exists as the first property.
In Flutter, a Key can uniquely identify a widget, just as its name suggests, a Key is primarily used for unique identification.
We have not used this Key before, which leads to two situations:
- Different component types: For example,
Container
andSizedBox
. In this case, Flutter can distinguish between components by their types, so there is no need to use a key. - Same component types: For example, an array composed of multiple
Container
widgets. In this case, Flutter cannot distinguish between multiple components by their types, so a Key is needed.
By default, if no key is defined, Flutter does not automatically create a key, but uses the widget's type and index to call.
We can describe the problem caused by not using a key through an example:
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
List<Widget> list = [];
@override
void initState() {
super.initState();
list = [
const Box(color: Colors.blue),
const Box(color: Colors.yellow),
const Box(color: Colors.red),
];
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Title'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: list,
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
setState(() {
list.shuffle(); // shuffle can randomly rearrange the elements of this list
});
},
child: const Icon(Icons.refresh),
),
);
}
}
class Box extends StatefulWidget {
final Color color;
const Box({Key? key, required this.color}) : super(key: key);
@override
State<Box> createState() => _BoxState();
}
class _BoxState extends State<Box> {
int _count = 0;
@override
Widget build(BuildContext context) {
return Container(
width: 100,
height: 100,
color: Colors.transparent,
child: ElevatedButton(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(widget.color)),
onPressed: () {
setState(() {
_count++;
});
},
child: Text(
"$_count",
style: Theme.of(context).textTheme.headlineLarge,
),
),
);
}
}
The above is an example of a StatefulWidget. By clicking the floatingActionButton
, the order of the three Boxes displayed on the screen is shuffled, resulting in the following effect:
(image)
As you can see, although clicking the shuffle button changes the order of the boxes on the surface, the internal State, which is the display of the numbers, does not change accordingly. This is because Flutter recognizes that these three Boxes are the same and only makes changes at the stateless level, while the contents of the State do not move, which causes this phenomenon (this explanation might not be very clear, I will supplement it later).
At this point, we need to use a key to help us:
Key as a Unique Identifier#
Keys can be divided into two main categories: GlobalKey
and LocalKey
. As the names suggest, Global is global, and Local is local (non-global).
// GlobalKey can be shared for global use
final GlobalKey _globalKey1 = GlobalKey();
final GlobalKey _globalKey2 = GlobalKey();
final GlobalKey _globalKey3 = GlobalKey();
// GlobalKey is very expensive and should be used judiciously
// LocalKey
// ValueKey is a LocalKey
const Box(color: Colors.blue, key: ValueKey(1)),
// ObjectKey is also a LocalKey
const Box(color: Colors.red, key: ObjectKey(Text("key"))),
// UniqueKey is also a LocalKey, serving as a unique existence (generating a unique hash code)
Box(color: Colors.red, key: UniqueKey()),
// PageStorageKey can save the current state of the page, such as the scroll state of a list
PageStorageKey()
The differences between LocalKey
and GlobalKey
:
LocalKey
is applied in situations where there are components with the same parent node, meaning one parent node manages multiple child nodes, and child nodes should preferably useLocalKey
.GlobalKey
can access state across widgets and is applied in situations where there are multiple parent nodes, meaning child nodes correspond to multiple parent nodes. For example, the horizontal and vertical screen switching function mentioned later requires switching betweenRow
andColumn
. If onlyLocalKey
is used, the existing state ofState
will be lost.
The above describes how to define GlobalKey
and LocalKey
.
Now let's add Keys to the Boxes in the above example:
The following example will use the orientation
property in MediaQuery
to query the device's orientation status.
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
// GlobalKey can be shared globally, so when switching between portrait and landscape, the state of the components will not be lost
final GlobalKey _globalKey1 = GlobalKey();
final GlobalKey _globalKey2 = GlobalKey();
final GlobalKey _globalKey3 = GlobalKey();
List<Widget> list = [];
@override
void initState() {
super.initState();
list = [
Box(color: Colors.blue, key: _globalKey1),
Box(color: Colors.yellow, key: _globalKey2),
Box(color: Colors.red, key: _globalKey3),
];
}
// If we use localKey (ValueKey), we will lose the state
// List<Widget> list = [
// const Box(color: Colors.blue, key: ValueKey(1)),
// const Box(color: Colors.yellow, key: ValueKey(2)),
// const Box(color: Colors.red, key: ValueKey(3)),
// ];
@override
Widget build(BuildContext context) {
// Portrait is vertical, landscape is horizontal
print(MediaQuery.of(context).orientation);
return Scaffold(
appBar: AppBar(
title: const Text('Title'),
),
body: Center(
// Display horizontally in landscape, vertically in portrait
// However, after this change, the state cannot be preserved because the keys of Column and Row and the components themselves are different
child: MediaQuery.of(context).orientation == Orientation.portrait
? Column(
mainAxisAlignment: MainAxisAlignment.center,
children: list,
)
: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: list,
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
setState(() {
list.shuffle(); // shuffle can randomly rearrange the elements of this list
});
},
child: const Icon(Icons.refresh),
),
);
}
}
The effect is as follows:
Note: Using
GlobalKey
has a high cost, and if there are other options available, it should be avoided as much as possible. Additionally, the sameGlobalKey
must be unique throughout the widget tree and cannot be duplicated.
Using GlobalKey to Operate Child Elements#
A very important feature of GlobalKey is that it can access the State across widgets.
For example, if you want to get the State inside a child widget (Box), you can do it like this:
var boxState = _globalKey.currentState as _BoxState;
Once you have the boxState
, you can directly call methods and properties from BoxState
.
Additionally, you can also get the child widget node:
var boxWidget = _globalKey.currentWidget as Box;
This way, you can access various properties of the box, such as background color, width, height, font size, etc.
Complete demonstration:
// Parent widget
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
// GlobalKey can be shared globally
final GlobalKey _globalKey = GlobalKey();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Title'),
),
body: Center(
child: Box(color: Colors.blue, key: _globalKey),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
// Parent component gets the state of the child widget
var boxState = _globalKey.currentState as _BoxState;
setState(() {
boxState._count++;
});
print(boxState._count);
// Call a method from the child widget
boxState.run();
// Get the child widget
var boxWidget = _globalKey.currentWidget as Box;
print(boxWidget.color); // MaterialColor(primary value: Color(0xff2196f3))
// Get the rendered properties of the child component
var renderBox =
_globalKey.currentContext!.findRenderObject() as RenderBox;
print(renderBox.size); // Size(100.0, 100.0)
},
child: const Icon(Icons.add),
),
);
}
}
// Child widget
class Box extends StatefulWidget {
final Color color;
const Box({Key? key, required this.color}) : super(key: key);
@override
State<Box> createState() => _BoxState();
}
class _BoxState extends State<Box> {
int _count = 0;
void run() {
print("I am the run method of the box");
}
@override
Widget build(BuildContext context) {
return Container(
width: 100,
height: 100,
color: Colors.transparent,
child: ElevatedButton(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(widget.color)),
onPressed: () {
setState(() {
_count++;
});
},
child: Text(
"$_count",
style: Theme.of(context).textTheme.headlineLarge,
),
),
);
}
}