tl;dr
- Keys uniquely identify elements in the widget tree
- Keys are necessary to enhance the performance of widget matching, which improves the efficiency of rebuilding the widget tree
- Keys are beneficial for reordering child widgets, such as in
AnimatedList
andListView
- There are different types of keys including
LocalKey
s andGlobalKey
s which should be used in different situations
Keys are an interesting topic in Flutter. Especially when starting to learn it, you can easily get around understanding and even using it.
Most often, you’ll have an unexplainable bug in your application when you realize the root cause is not having used keys.
This is when you know it’s about time to get a better understanding of it.
What are keys in Flutter?
The main purpose of keys are to maintain a widget’s state even if it’s moved or duplicated inside the widget tree.
It’s a core topic to understand as a Flutter developer because there are several situations in which keys are necessary to maintain the robustness of the application.
Not using keys at all can lead to undesired behavior that shows subtly during the usage of your apps. In order to prevent these unwanted UX glitches, it’s good to know the meaning of keys.
To sum it up, in Flutter, a key is a unique identifier for a widget. It aids the framework in tracking widgets in the widget tree.
Why is this necessary?
When you think about it, you might come to the bottom line question: if it’s so hard for beginners to understand and often leads to errors, why did they make it like that? Why is it necessary to provide keys? Couldn’t the framework handle this on its own?
To answer it in one word: performance.
This has to do with the way, Flutter decides whether it should re-render a widget or not. This is crucial from a performance perspective because re-rendering is a very expensive operation in terms of CPU time.
Element tree and widget tree
In order to dive a little bit deeper into the topic of re-rendering widgets, let’s talk about the element tree and the widget tree.
Like I have already mentioned in my article about BuildContext
, you need to understand the fundamental difference between widgets and elements to fully grasp the underlying mechanics.
In Flutter, keys are necessary because they provide a way to make the element tree more efficient and improve performance.
Flutter builds a visual tree comprising of mutable copies of widgets, called Element
s. However, in the usual course of app development, developers don’t interact directly with Element
s as the framework handles them.
Each widget in Flutter has a corresponding Element
instance. When a widget rebuilds, the framework creates a new widget tree and a new element tree. The element tree is used to map the widget hierarchy to the rendering pipeline, which generates output for the app.
Without keys, the framework has to check each widget in the new tree against each widget in the old tree to determine whether they are the same, have moved, or have been removed.
However, with keys, the framework can identify which elements in the new tree correspond to the same elements in the old tree. This reduces the number of widget comparisons needed during the widget matching phase, which can lead to significant performance improvements.
In addition, keys have other benefits such as improving the efficiency of AnimatedList
, ListView
and other widgets that can re-order their child widgets. By assigning keys to individual list items, Flutter can more efficiently identify when items are added, removed, or updated, and animate those changes more smoothly.
In summary, keys are necessary in Flutter for performance reasons. By providing a way to more efficiently update the element tree, Flutter can reduce the amount of work the framework needs to do when rebuilding the widget tree, resulting in improved performance and a smoother user experience.
When to use Keys?
If you find yourself in any way altering (e. g. adding, removing, reordering) a list of widgets of the same type that hold state, chances are high you need any type of key.
Keys should be used with particular widgets such as ListView
and some stateful widgets that need to maintain their state across page navigations.
For example, consider a dog sitting app that displays a list of offers 🐶. We imagine these offers being part of a ListView
.
So far so good. Every element in the ListView
, which we name SittingOfferTile
, has a representation in the widget tree and thus in the element tree.
Let’s say the list can be sorted by different criteria, such as payment or distance. In this case, a key can be used to ensure that each item maintains its state even when the list is sorted. Without keys, the item state would not be preserved, and it would be challenging to maintain the order and state of the list.
Let’s pretend the user has chosen a sort method which swapped the order of the first two offers and have a look at how things prevent themselves from Flutter’s perspective.
In the above image we can see the status quo: before the sorting, the first SittingOfferTile
in the widget tree (the offer for 12 €) is the first entry in the element tree. The second element behaves analogously.
Now comes the crucial part: after the sorting has taken effect, from Flutter’s perspective, nothing has changed which prevents it from re-rendering.
This is because Flutter doesn’t take the actual content of the widget into account but only looks at the runtime type.
Before:
First and second child of the ListView
have the runtime type SittingOfferTiles
.
After:
First and second child of the ListView
still have the runtime type SittingOfferTiles
.
→ It seems like nothing has changed! No reason to re-render.
This is where keys come in handy: they provide a way for Flutter to keep track of a widget regardless of their type. By assigning a unique key, Flutter knows: “Ah, the widget at position 3 has they key ‘abc’. It must be the same widget that as been at position 1 in the last render because the widget as position 1 used to have the very same key (‘abc’).”
Key
will only affect the direct cildren of the ListView
.Different types of keys
Okay great. We have figured out that keys are the solution to the problem of Flutter’s framework not being able to distinguish multiple widgets of the same type inside a list.
The question that comes up is: What keys should we give these widgets?
1, 2, 3? A, b, c? Or even something randomly generated?
This is where key types come into play.
There are a bunch of Keys
you can use. The central questions you should ask yourself when deciding which one to use are:
- What kind of data do I have to uniquely identify a widget?
- In which scope is uniqueness defined here?
We will discuss these two questions for every type of Key
with regard to our dog sitting app example.
Keys
how I have explained them so far are only a concept. Since we are in an object-oriented language, each Key
corresponds to a class. This means, every type of Key
has a corresponding class.
Key
Let’s start with the simplest one: Key
.Key
is the abstract class all other Keys
inherit from. That means you can’t instantiate it.
However, the class provides a factory constructor that shadows the default constructor and redirects to ValueKey, which we will look at later.
This means, if you try to instantiate it like this: Key key = Key('…')
, the runtime type of this object will be ValueKey<String>
.
This is how the class is defined:
1@immutable
2abstract class Key {
3 const factory Key(String value) = ValueKey<String>;
4
5 @protected
6 const Key.empty();
7}
As you can see, the Key
factory exclusively accepts a single String
argument.
Going back to our example, let’s say the above mentioned SittingOfferTiles
are created from a model called SittingOffer
, which is defined as follows:
1class SittingOffer {
2 final String name;
3 final double distance;
4 final DateTime start;
5 final DateTime end;
6 final double hourlyPayment;
7}
Now we could just use the name
property of the SittingOffer
class as it’s the only String
property of the class.
We could then utilize it like this in the ListView
:
1ListView(
2 children: offers
3 .map((SittingOffer offer) => SittingOfferTile(key: Key(offer.name), offer: offer))
4 .toList(),
5)
The issue we have here is that we assume uniqueness regarding the name
. If the same name appears more than once, we will have the same issue with these duplicated entries because Dart will treat them as equal and won’t notice them swapping.
ObjectKey
Having used the name
property as the Key
seems like an arbitrary decision. Having to explicitly choose a certain String
property of the underlying model can be circumvented by using what is called an ObjectKey
.
Like the name implies, the ObjectKey
uses the whole object for comparison instead of only one of its values. In this case it would compare the SittingOffers
.
We could change our code change to make use of it:
1ListView(
2 children: offers
3 .map((SittingOffer offer) => SittingOfferTile(key: ObjectKey(offer), offer: offer))
4 .toList(),
5)
Its only constructor argument is of type Object?
which means that it will work with every kind of object.
Using this kind of Key
is useful for situations where multiple widgets may have the same values but are actually separate instances.
ObjectKey
uses instance comparison with the identity function to determine equality, implying that the same object in terms of memory address will be regarded as equal.ValueKey
If you have a scenario in which the same object appears multiple times within a ListView
, the ObjectKey
might not be the best choice. As when re-ordering happens, Flutter will be unable to distinguish between those objects.
In this case, a ValueKey
could be a better choice. Instead of using the identity function, it utilizes the the == operator. Although it defaults to the same behavior as the identity function, it can be overridden.
So if you have a custom way of checking equality for your very own class then it will use this way of comparing.
It also plays nice with packages like equatable.
1ListView(
2 children: offers
3 .map((SittingOffer offer) => SittingOfferTile(key: ValueKey<SittingOffer>(offer), offer: offer))
4 .toList(),
5)
UniqueKey
Let’s say instead of dog sitting offers, you have a much simpler list with elements that do not have an underlying model.
This can be for example a list of colors you have stored locally where each element previews the color and the same color can occur more than once.
There is no such thing as an id and you also can not use the Color
object as the Key
as it can be the same for duplicated colors.
In this case the UniqueKey
might be the best option. UniqueKey
creates a key that is equal only to itself. In comparison to other Key
types that have already been mentioned, this one does not offer a const
constructor as it would imply that all instantiated keys were the same instance and therefore not unique.
This key type is generated randomly and uniquely whenever a widget is created. It is particularly useful when creating a widget multiple times with different properties, where we can assign different keys.
It’s also the only Key
that doesn’t have a constructor argument.
We would use it like this:
1ListView(
2 children: colors
3 .map((Color color) => ColorTile(key: UniqueKey, color: color))
4 .toList(),
5)
LocalKey
For the sake of completeness: LocalKey
is a subclass of Key
and the superclass for all of the other Keys
we have looked at so far.
It’s used to identify widgets within the same parent widget. Local keys cannot be used to identify widgets outside of their parent.
LocalKey
itself is abstract and there is no way to directly instantiate it.
GlobalKey
This is a type of Key
that can be used to identify a widget from anywhere in the widget tree. Unlike LocalKeys
, which can only identify widgets within the same parent, GlobalKey
can identify widgets from anywhere in the widget tree.
It’s an abstract class but it offers a factory constructor for instantiation.
Concrete usage examples are:
- Moving widgets from one parent to another preserving it’s state
- Form validation
- Displaying the very same widget in multiple screens and holding its state
- Using an AnimatedList (adding or removing elements from outside the list)
LabeledGlobalKey
There is one subclass of GlobalKey
which is called LabeledGlobalKey
. It’s not a completely new Key
type but instead used to give the GlobalKey
a label which can be used for debugging.
Using the only constructor argument of GlobalKey
also utilizes LabeledGlobalKey
and uses the argument given as the debug label.
GlobalKey
and LabeledGlobalKey
is only a debug argument, it’s not used for comparing the identity. It’s purely a convenience concept for the developers.Conclusion
In conclusion, keys are a crucial component of Flutter applications, and they must be used appropriately to ensure that widgets maintain their state and can be efficiently updated or removed from the widget tree.
Keys are used to ensure that the framework can identify and track widgets, even if they are moved or duplicated in the tree.
When working with dynamic widgets, animations, or duplicate widgets, keys must be used to maintain the state of the widget and ensure that the app works as expected.
In most situations, you will have a list of items that have an underlying model most often coming from an API or some kind of data source. In this case, you can just use an ObjectKey
in which you put this model.
If there is no such thing as an identifier and the list is generated “on the fly”, then a UniqueKey
is most often the best solution.
Lastly, when you have a situation, in which you need to access items from all over the app or at least not only from the parent widget, then a GlobalKey
can be useful. This can be the case for form validation or AnimatedLists
.
Zachary Russell
Marc
In reply to Zachary Russell's comment
Azim Otajonov