Using Keys in Flutter

Explaining the different types

Using Keys in Flutter
No time to read?
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 and ListView
  • There are different types of keys including LocalKeys and GlobalKeys 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 Elements. However, in the usual course of app development, developers don’t interact directly with Elements 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.

A diagram showing the widget tree, the element tree and the render tree
The trees and their connection

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.

An iconic screenshot of an imaginary dog sitting app
Our imaginary dog sitting app containing 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.

The connection between the widgets and the elements of our app
The connection between the widgets and the elements of our app

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.

An iconic screenshot of the sort sheet of our dog sitting app
The list of offers can be sorted

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.

Widget tree before swapping
Both of the widgets have their proper representation on the element tree

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.

Widget tree after swapping the two `SittingOfferTiles`
The runtime type and the position in the tree is the same after swapping

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’).”

Widget tree after swapping the two `SittingOfferTiles` with key usage
When using keys, the reference is preserved
Tip
Flutter’s algorithm to check for possible re-renderings only looks ahead one level in the tree at a time. That means that using a 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.

Tip
Note that 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.

Keep in Mind
Since the constructor argument of the 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.

Comments (3) ✍️

Zachary Russell

Fantastic blog post! This comprehensive guide on keys in Flutter is extremely helpful for developers at all levels. The explanation of each key type, accompanied by practical examples, makes it easy to understand when and how to use them in real-world applications. The dog sitting app example is particularly useful in demonstrating how keys can maintain state in a ListView.

I also appreciate the focus on performance improvements when using keys, as this is an essential aspect of mobile app development. This post not only clarifies the concept of keys for those new to Flutter but also serves as a valuable reference for experienced developers.

Thank you for sharing your knowledge and expertise on this topic. I’m excited to see more insightful blog posts like this in the future!

Reply to Zachary Russell

Marc
In reply to Zachary Russell's comment

Hey Zachary,

thank you for the appreciating comment 🙂.

I am always happy to receive positive feedback and I am glad I could help you understand the concept of keys.

I will try to keep up the effort to teach ideas and concepts of the Flutter world to inquisitive Flutter developers.

Reply to Marc

Azim Otajonov

Thank you a lot. This is the best blog I have ever seen about keys. Keep making this kind of blogs. It is really helpful for all developers.
Reply to Azim Otajonov

Comment this 🤌

You are replying to 's commentRemove reference