The original:Keys! What are they good for?
The author:Emily Fortuna
Almost every widget constructor has key parameters, but their use is less common. Keys preserve the state of the widget as it moves on the Widget Tree. In practical use, this means that keys are useful for preserving a user’s scrolling state or for preserving the state when a collection needs to be modified.
This article is adapted from the official Flutter video: When to Use Keys – Flutter Widgets 101 Episode 4
If you prefer to watch video, this video covers everything covered in this article.
Inside information on Key
In most cases, we don’t need keys! In general, adding keys doesn’t hurt, but it doesn’t help either. It just takes up unnecessary space. Just like when you use the new keyword in Dart, or when you define a new variable you declare the type on both the left and right sides of the expression. However, if you want to add, remove, sort, and so on on a stateful collection of widgets of the same type, you may need to use keys.
To illustrate why you need keys when modifying a widget collection, I wrote a very simple application that has two widgets with a random background color that swap positions when buttons are clicked:
In the stateless version, two stateless StatelessColorfultiles with random background colors are placed in a Row, Store the location of these tiles using PositionedTiles inherited from the StatefulWidget. When clicking on the bottom FloatingActionButton, tile positions in the list are swapped correctly:
void main() => runApp(new MaterialApp(home: PositionedTiles()));
class PositionedTiles extends StatefulWidget {
@override
State<StatefulWidget> createState() => PositionedTilesState();
}
class PositionedTilesState extends State<PositionedTiles> {
List<Widget> tiles = [
StatelessColorfulTile(),
StatelessColorfulTile(),
];
@override
Widget build(BuildContext context) {
return Scaffold(
body: Row(children: tiles),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.sentiment_very_satisfied), onPressed: swapTiles),
);
}
swapTiles() {
setState(() {
tiles.insert(1, tiles.removeAt(0)); }); }}class StatelessColorfulTile extends StatelessWidget {
Color myColor = UniqueColorGenerator.getColor();
@override
Widget build(BuildContext context) {
return Container(
color: myColor, child: Padding(padding: EdgeInsets.all(70.0))); }}Copy the code
But when a button was clicked to replace a tile with a stateful StatefulColorfulTile and store the color in State, the interface looked the same.
List<Widget> tiles = [ StatefulColorfulTile(), StatefulColorfulTile(), ]; .class StatefulColorfulTile extends StatefulWidget {
@override
ColorfulTileState createState() => ColorfulTileState();
}
class ColorfulTileState extends State<ColorfulTile> {
Color myColor;
@override
void initState() {
super.initState();
myColor = UniqueColorGenerator.getColor();
}
@override
Widget build(BuildContext context) {
return Container(
color: myColor,
child: Padding(
padding: EdgeInsets.all(70.0))); }}Copy the code
As a reminder, the code shown above is problematic. Because when the user presses the “swap” button, the color blocks are not swapped. The solution was to add a key parameter to stateful widgets. The widgets then start swapping places correctly as we expect:
List<Widget> tiles = [
StatefulColorfulTile(key: UniqueKey()), // Keys added hereStatefulColorfulTile(key: UniqueKey()), ]; .class StatefulColorfulTile extends StatefulWidget {
StatefulColorfulTile({Key key}) : super(key: key); // NEW CONSTRUCTOR
@override
ColorfulTileState createState() => ColorfulTileState();
}
class ColorfulTileState extends State<ColorfulTile> {
Color myColor;
@override
void initState() {
super.initState();
myColor = UniqueColorGenerator.getColor();
}
@override
Widget build(BuildContext context) {
return Container(
color: myColor,
child: Padding(
padding: EdgeInsets.all(70.0))); }}Copy the code
A key was required to maintain the widget status when a stateful widget was created in a subtree. If the widget subtree is full of stateless widgets, keys are not required.
Technically, that’s all you need to know to use key in Flutter. But if you want to understand the root cause of all this…
Why is a Key sometimes needed
You’re still here, huh? Well, go ahead and learn the essence of widgets and Element trees and become a Flutter wizard!
As we all know, Flutter builds a corresponding Element for each widget. Just as Flutter builds the Widget tree, Flutter also builds the Element tree. ElementTree is simple; it simply holds the widget’s type information and references to child elements. You can think of ElementTree as the skeleton of the Flutter application. It shows the structure of the application, and all additional information can be found by referring to the original widget.
The Row widget in the above example actually holds an ordered set of slots for each of its child widgets. When swapping the order of Tile widgets in a Row, Flutter traverses the ElementTree to see if the skeleton structure is the same.
The child elements are iterated, starting with RowElement. RowElement checks whether the type and key of the new widget are the same as the old widget it holds, and if they are, references the new widget. In the stateless version, the widget does not have a key, so the Flutter only checks if the type is the same. (If this seems too much information, check out the GIF above.)
The structure of the Element tree corresponding to stateful widgets looks a little different. Widgets and Elements are the same as in the stateless version, but with an associated state object. Color information about stateful widgets was stored in a State object, not in the widgets themselves.
When a stateful Tile is used and the order of two widgets is swapped without a key being passed in, Flutter looks at the ElementTree, checks the RowWidget’s type, and updates the reference. TileElement then checks to see if the type of the corresponding widget is the same and updates the widget reference. The types of widgets here are obviously the same. The same thing is done on the second child. Since Flutter uses ElementTree and its corresponding state object to determine what content should be displayed on your device, Weidget doesn’t exchange properly from our human perspective.
In the version that fixes this problem using stateful tiles, we add keys to the widget. Now that the widgets are swapped, the Row widgets match as in the previous version, but the Tile Element’s key does not match the corresponding Tile widget’s key. This causes the Flutter to start with the first mismatched Element, deactivate those mismatched elements, and remove references to those Tile elements from the Element Tree.
Flutter then looks for the element with the correct key in the Row’s mismatched child elements. When a match is found, update element’s reference to the widget. Then, do the same with the second child. The Flutter will now display as we expect, and when the button is pressed, the widgets will swap positions and update their colors.
In summary, keys are useful when changing the order or number of stateful widgets in a collection. For illustrative purposes, this example stores colors as states. But states are often much more obscure than that. Playing animations, displaying user-entered data, and scrolling positions all involve states.
Where should the Key go?
Simply put: If you need to add a key to your app, you should add the key to the top of the Widget subtree where you want to maintain state.
A common mistake I’ve seen is when people think they only need to put a key on the first stateful widget, but “it’s dangerous here.” Don’t believe me? To show what kind of trouble I can get into, I wrap the colourfulTile widget with the Padding widget, but leave the key on the tiles.
void main() => runApp(new MaterialApp(home: PositionedTiles()));
class PositionedTiles extends StatefulWidget {
@override
State<StatefulWidget> createState() => PositionedTilesState();
}
class PositionedTilesState extends State<PositionedTiles> {
// Stateful tiles now wrapped in padding (a stateless widget) to increase height
// of widget tree and show why keys are needed at the Padding level.
List<Widget> tiles = [
Padding(
padding: const EdgeInsets.all(8.0),
child: StatefulColorfulTile(key: UniqueKey()),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: StatefulColorfulTile(key: UniqueKey()),
),
];
@override
Widget build(BuildContext context) {
return Scaffold(
body: Row(children: tiles),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.sentiment_very_satisfied), onPressed: swapTiles),
);
}
swapTiles() {
setState(() {
tiles.insert(1, tiles.removeAt(0)); }); }}class StatefulColorfulTile extends StatefulWidget {
StatefulColorfulTile({Key key}) : super(key: key);
@override
ColorfulTileState createState() => ColorfulTileState();
}
class ColorfulTileState extends State<ColorfulTile> {
Color myColor;
@override
void initState() {
super.initState();
myColor = UniqueColorGenerator.getColor();
}
@override
Widget build(BuildContext context) {
return Container(
color: myColor,
child: Padding(
padding: EdgeInsets.all(70.0))); }}Copy the code
At this point, click the button and the Tiles become a completely different random color!
The following figure shows WidgetTree and ElementTree wrapped around the Padding widget:
The element-to-widget matching logic of a Flutter checks only one level of the tree at a time when the location of child nodes is swapped. In the figure below, the grandchildren (children of children) are grayed so that we can focus on only one level at a time. The first layer where the Padding Elements are, everything matches correctly.
On layer 2, a Flutter finds that the Tile Element’s key is not the same as the Tile Widget’s key, deactivates the Tile Element, and dismisses the connections. This example uses LocalKeys, which means that Flutter only looks for key matches at a particular level of the tree when matching widgets to Elements.
Since no tile Element with that key can be found in this layer, a new element is created and initialized to a new state, and in this case, the widget turns orange!
If you add a key to the Padding Widget level:
void main() => runApp(new MaterialApp(home: PositionedTiles()));
class PositionedTiles extends StatefulWidget {
@override
State<StatefulWidget> createState() => PositionedTilesState();
}
class PositionedTilesState extends State<PositionedTiles> {
List<Widget> tiles = [
Padding(
// Place the keys at the *top* of the tree of the items in the collection.
key: UniqueKey(),
padding: const EdgeInsets.all(8.0),
child: StatefulColorfulTile(),
),
Padding(
key: UniqueKey(),
padding: const EdgeInsets.all(8.0),
child: StatefulColorfulTile(),
),
];
@override
Widget build(BuildContext context) {
return Scaffold(
body: Row(children: tiles),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.sentiment_very_satisfied), onPressed: swapTiles),
);
}
swapTiles() {
setState(() {
tiles.insert(1, tiles.removeAt(0)); }); }}class StatefulColorfulTile extends StatefulWidget {
StatefulColorfulTile({Key key}) : super(key: key);
@override
ColorfulTileState createState() => ColorfulTileState();
}
class ColorfulTileState extends State<ColorfulTile> {
Color myColor;
@override
void initState() {
super.initState();
myColor = UniqueColorGenerator.getColor();
}
@override
Widget build(BuildContext context) {
return Container(
color: myColor,
child: Padding(
padding: EdgeInsets.all(70.0))); }}Copy the code
Flutter correctly updates the connection, as we did in the previous example. Order was restored to the universe.
What Key should be used?
Good Flutter APIs providers have several types of keys ready for us to use. Which types of keys are used depends on the characteristics of the items that require them. Take a look at the information stored in these widgets. Here, we’ll discuss four different types of keys: ValueKey, ObjectKey, UniqueKey, and GlobalKey.
Consider the following To-do List app^1, which rearranges items in the TODO list by priority and removes TODO items when done.
In this scenario, assuming that the text of the TODO item is immutable and unique, this is a good scenario for using ValueKey. The text is the value of ValueKey.
return TodoItem(
key: ValueKey(todo.task),
todo: todo,
onDismissed: (direction) => _removeTodo(context, todo),
);
Copy the code
Another scenario: There is an address book app that lists information for each user. In this case, each child widget stores complex composite data. Any single data field, such as name or date of birth, may be the same as the corresponding field in another entry, but the combination of all fields is unique. In this case, ObjectKey is probably the most appropriate choice.
You can use UniqueKey if you have multiple widgets with the same value in the collection, or if you want to really ensure that each widget is different from the others. UniqueKey is used in the color switch example app because no constant data is stored in the tile and you don’t know what color is when you create the widget. Be careful with UniqueKey though! If the UniqueKey is created inside the build method, the widget gets a different UniqueKey instance each time the build method is re-executed. This will be no different than not using key!
Similarly, you never want to use a random number as a Key. Each time a widget is built, a new random number is produced, and the consistency between frames is lost. Might as well not have used Key in the first place.
PageStorageKey is a special key used to store the user’s scrolling position so that the app can keep the current scrolling position for future use.
The GlobalKey serves two purposes: first, it allows widgets to change parent anywhere in the app without losing data; The other is information that can be used to access another widget in a completely different part of the Widget Tree. An example of the first scenario is to use GlobalKey if you want to display the same widget on different pages and keep the widget in the same state. In the second scenario, you assume that the password is validated, but you don’t want to share that state information with other widgets in the tree. GlobalKey can also be used for testing by using a key to access a specific widget and query for status information about it.
Usually (but not always!) GlobalKey is a bit like a global variable. There is a better way to view state in Flutter, which is to use an InheritedWidget, or something like Redux, BLoC mode.
Quickly review
In summary, use Key when you want to preserve state across the widget tree. The most common scenario is to modify a collection of widgets of the same type, such as a list. Place the key at the top of the Widget tree where you want to preserve the state, and choose the type of key to use based on the data stored in the widget.
Congratulations, you are on your way to become the wizard of Flutter! Oh, did I say sorcerer? I mean sourcerer, like people who write application source code… It’s almost as good. … Almost. ⚡
reference
[1]: Code for the To-do app was inspired by Vanilla Example