Keys In Flutter - UniqueKey, ValueKey, ObjectKey, PageStorageKey, GlobalKey
All you need to know about Keys in Flutter
Introduction
- Flutter has many widgets in its library, and if you take a look at every widget's properties you'll most probably find the
key
parameter in all of those widgets. - Most of the beginners who started learning Flutter don't know about
Keys
. And if they do, they don't know how to use them or where to put them. - It's because
Keys
has very few use cases or you can say their use is less common. - In this blog, I will try to explain the concept of
Keys
, and different types ofKeys
, Where to useKeys
, and How to use them.
What is Key
??
- If we take a look at the definition of
Keys
written in Flutter's Official Documentation, It says :A Key is an identifier for Widgets, Elements, and SemanticsNodes.
- It simply means is Flutter basically identifies the widgets and where it is placed in widget tree by
Keys
. But it's more than that. Keys
preserves thestate
when you move around the widget tree.If you find yourself adding, removing, or reordering a collection of widgets of the same type that hold some state, using keys is likely in your future.
- Let's take a look at different types of keys one by one.
UniqueKey :
- The
UniqueKey
in Flutter is used to identify every widget of your app uniquely. UniqueKey
also preserves the state when widgets move around in your widget tree.UniqueKey
can be used in cases like when you are reordering the widget in the list or adding or removing the widgets from a list.- It is helpful when you have multiple widgets in your widget tree with the same values and same type and you want to identify each of them uniquely.
- It is also helpful when a unique id is not defined in your DB collection to identify all the list of items uniquely. You can use
UniqueKey
which will assign a unique key to that particular widget. - Let's take one example
- I'm going to create an emoji swapper program, where there are going to be two emojis displayed on the screen, and a button underneath them, which will swap the position of the emojis when triggered.
class HomePage extends StatefulWidget { @override _HomePageState createState() => _HomePageState(); }
class _HomePageState extends State<HomePage> { List<Widget> emojis = [ GetEmoji(emoji: "😎"), GetEmoji(emoji: "🤠") ]; swapEmoji() { setState(() { emojis.insert(1, emojis.removeAt(0)); }); } @override Widget build(BuildContext context) { return Scaffold( body: SizedBox.expand( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Row( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: emojis, ), SizedBox( height: 20, ), ElevatedButton( onPressed: swapEmoji, child: Text("Swap"), ) ], ), )); } }
class GetEmoji extends StatelessWidget { GetEmoji({required this.emoji}); String emoji; @override Widget build(BuildContext context) { return Text( emoji, style: TextStyle( fontSize: 100, ), ); } }
- And as you can expect, the emojis will swap their position when we click the swap button
But the problem will come if we try to convert the
Stateless
widget intoStateful
widget and store the value in astate
.- Let's see what will happen if we convert it.
class GetEmoji extends StatefulWidget { GetEmoji({required this.emg}); String emg; @override _GetEmojiState createState() => _GetEmojiState(); }
class _GetEmojiState extends State<GetEmoji> { late String emoji; @override void initState() { super.initState(); emoji = widget.emg; } @override Widget build(BuildContext context) { return Text( emoji, style: TextStyle( fontSize: 100, ), ); } }
- Now nothing is happening. The emojis are no longer changing their positions.
- It's because under the hood Flutter distinguishes widget by the
type
of the widget (runtimeType
) and by thekeys
. - We've two
Stateful
widgets in our list (one for 😎, and one for 🤠). when we swap the emojis positions, by pressing theswap
button, flutter will then check in theElementTree
that, Is thetype
of the changed widget is the same as thetype
of theElementTree
's element or not?. - If you don't know about
ElementTree
, TheElementTree
only holds the information about the type of each widget and a reference to children's elements. You can think of the ElementTree as a skeleton of your Flutter app. It shows the structure of your app.
- After swapping it will check the runtime type.
- When I've swapped the order of the two widgets, Flutter walks the
ElementTree
, checks the type of theRowWidget
, and updates the reference. After that, it checks if the type of😎 Text Element
of the ElementTree is same as🤠 Text Widget
'stype
? and it is, so it updates the reference. And nothing will update. - But..but If we name those two
Stateful
widgets differently. Then there will be no problem. Because both will then have different IDs/keys assigned. - To update the widget which has the same type inside the list, we have to pass the
UniqueKey
to all the widget class GetEmoji extends StatefulWidget { GetEmoji({required this.emg, required Key key}) : super(key: key); String emg; @override _GetEmojiState createState() => _GetEmojiState(); }
List<Widget> emojis = [ GetEmoji( emg: "😎", key: UniqueKey(), ), GetEmoji( emg: "🤠", key: UniqueKey(), ), ];
- Here what is happening is when flutter tries to match the
type
, its gets matched. But when it is trying to match keys it will not match. And in the element tree, askeys
are not matching, it will change the references and update the app. Swapping widget
Keys not matched
- Change references
- Swap of Elements in ElementTree
Where to put Keys
- NOTE one thing, That if you need to add keys to your app, you should add them at the
top
of the widget subtree. Otherwise, you'll get some weird results. - Let me explain with an example.
- Here I've used the previous example only. Just added a background color which is generated randomly.
- Everything is running.
- Now let's wrap our
GetEmoji
widget withContainer
widget. Now observe hereUniqueKey
is not at thetop
of its widget tree. List<Widget> emojis = [ Container( child: GetEmoji( emg: "😎", key: UniqueKey(), ), ), Container( child: GetEmoji( emg: "🤠", key: UniqueKey(), ), ), ];
- And now let's run the app again
- Here the Widgets are swapping, that's okay but the new color is generating again and again when the swap button is triggered.
- In fact, It is not only the new color that is generating again and again but also a new
Text Widget
is generating again and again in the widget tree, we are not able to see that because we're using two static emojis only. - We've already assigned the
Keys
to the widget RIGHT? But it's not about the keys which are creating a problem it's about theposition
of the keys. - So what is going on?.
Here is the structure of the Widget and Element Tree
Here when we perform the swap operation, Flutter’s element-to-widget-matching algorithm looks at only one level in the tree at a time. At that first level of children with the Padding elements, everything matches up correctly.
- At the second level, Flutter notices that the key of the
😎 Container Element
doesn’t match the key of the widget, so it deactivates that😎 Container Element
, dropping those connections. - The keys we’re using in this example are
LocalKeys
. That means that when matching up widgets to elements, Flutter only looks for key matches within a particular level in the tree. - Since it can’t find a
Container Element
at that level with thatkey
value, it creates a new one, and initializes a new state, in this case, making the widget with the random background color. - This we can solve by adding the
key
in thePadding
widget. - So moral of the story is YOU HAVE TO DEFINE THE KEY AT THE TOP OF THE WIDGET TREE.
ValueKey
:
- A key that uses a
value
of a particular type toidentify
itself. - The
ValueKey
is useful if we want to preserve the state of the Stateful widgets when they move around the widget tree. - We can use the
ValueKey
when we want to removeWidget
from the widget tree, or reordering the list. - Consider the below code, where there are 2
Textfield
widget. And we want to remove the lastTextfield
from the widget tree. bool showFavouriteFramework= true; //... Column( mainAxisAlignment: MainAxisAlignment.center, children: [ if (showFavouriteFramework) TextField( decoration: InputDecoration( border: OutlineInputBorder(), labelText: "Favourite Framework"), ), TextField( decoration: InputDecoration( border: OutlineInputBorder(), labelText: "Favourite Language"), ), SizedBox(height: 10), ElevatedButton( onPressed: () { setState(() { showFavouriteFramework = false; }); }, child: Text("Remove Favourite Framework field"), ) ], ),
- When we press the
Remove Favourite Framework field
button. - If you observe, we got the
Text
of Favourite Framework's Textfield i.eFlutter
in the Textfield of Favourite language's Textfield instead ofDart
. - NOTE this will only happen if you use the multiple Stateful widgets of the same type.
- What's happening here is, when we're removing the first Textfield, Flutter is not able to identify which TextField it has to remove, because both are of the same type.
- We've to provide those widgets unique values that can help flutter to identify that they are different.
- We can provide Unique values with the help of
ValueKey
. TextField( key: ValueKey("Framework"), decoration: InputDecoration( border: OutlineInputBorder(), labelText: "Favourite Framework" ), ), TextField( key: ValueKey("Language"), decoration: InputDecoration( border: OutlineInputBorder(), labelText: "Favourite Language" ), ),
- Now as we can see Favourite Language got its actual value as expected.
- Here Flutter will first check the type of those two widgets and check if it is the same or not, and it is. Then after it'll check if the
keys
are of the same type or not, and it's not. So flutter will update the state and references accordingly. - Here in the
ValueKey
you can provide any type of unique values, like,String
,int
,double
,Objects
, etc. But all the widgets must have unique values. That you should keep in mind. Otherwise, it'll not work.
One important thing is when we have a list of widgets inside
Listview
,Column
,Row
, try to avoid giving theindex
value coming from the list as thekey
.
ObjectKey
- A key that uses a reference of a particular type to identify itself.
- The
ObjectKey
is useful if we want to preserve the state of the Stateful widgets when they move around the widget tree. ObjectKey
can be used in cases like when you are reordering the widget in the list or adding or removing the widgets from a list.- Let's take an example.
- I've created a List of SuperHero objects from the class SuperHero.
late List<SuperHero> superHeroList; @override void initState() { superHeroList = [ SuperHero(movie: "Iron Man", name: "Tony Stark"), SuperHero(movie: "Hulk", name: "Bruce Banner"), SuperHero(movie: "Thor:Ragnarok ", name: "Thor"), ]; super.initState(); }
Scaffold( floatingActionButton: FloatingActionButton( onPressed: () { setState(() { superHeroList.insert(0, superHeroList.removeAt(1)); }); }, child: Icon(Icons.swap_calls), ), body: Center( child: Column( children: superHeroList .map<Widget>((hero) => HeroWidget(hero: hero)) .toList(), ), ), );
- This program will swap the first two item's position in the
superHeroList
. - But if we try to swap these two items, something is going wrong, here if you see the element's text is interchanging but the colors are not. It should also change, right? because the color is also connected with that list item.
- As we've discussed in the unique key, that flutter is not able to distinguish the widget because of the same type.
- Then what's the solution?
- You might think, we can use
ValueKey
, Right?. And yes you're right we can useValueKey
to distinguish the list widgets. But there will be an issue. Let's see what happen if we consider theValueKey
in this situation. - Giving the
ValueKey
tokey
parameter. Center( child: Column( children: superHeroList .map<Widget>( (hero) => HeroWidget( key: ValueKey(hero), hero: hero, ), ) .toList(), ), ),
- And we'll get the result as expected. The items are swapping with color now.
-
-BUT...but, Now add the same
Object
in the list. superHeroList = [ SuperHero(movie: "Iron Man", name: "Tony Stark"), SuperHero(movie: "Iron Man", name: "Tony Stark"), SuperHero(movie: "Hulk", name: "Bruce Banner"), SuperHero(movie: "Thor:Ragnarok ", name: "Thor"), ];
- And the output is........
- Flutter will throw an error something like this :
- And it's correct because what we've seen in the
ValueKey
explanation is that the `widget is identified by its value when we use ValueKey - Here, in this case, we've added two same objects with the same value. That's why Flutter is throwing an error that Hey, I found duplicate keys.
- In such cases, we have to use
ObjectKey
. - As we've seen in the definition of the
ObjectKey
, thatObjectKey
will distinguish the item based on the references. - So let's try to add
ObjectKey
in thekey
parameter. HeroWidget( key: ObjectKey(hero), hero: hero, ),
- And as soon as we add the
ObjectKey
we can see the output. And all the things are working fine now.
PageStorageKey
- The
PageStorageKey
is basically used to store the scroll position of the scrollable widgets likeListView
,GridView
etc. - In some cases, we want to provide the functionality of storing the position of the scrolling list item and when users came back later to that scroll view they can start scrolling where they left.
- In this case, we can use
PageStorageKey
to preserve the state of the scrolling position. - Let's take an example to understand how we can use
PageStorageKey
in our app. Scaffold( body: ListView.builder( itemCount: 100, itemBuilder: (context, index) { return Padding( padding: const EdgeInsets.all(8.0), child: Text( "Item : $index", style: TextStyle(fontSize: 22), ), ); }, ), );
- Here I've created a simple Listview
- Now let's go to another tab while leaving the scroll position in the middle.
- See? It's not preserving the position of the listview.
- Let's try to solve this issue by adding
PageStorageKey
in the ListView'skey
parameter ListView.builder( key: PageStorageKey<String>("listViewKey"), itemCount: 100, itemBuilder: (context, index) => ListTile( title: Text( 'List item ${index + 1}', style: TextStyle(fontSize: 24), ), ), );
- THAT'S!! That's all you have to do to store the scroll location. Flutter will handle all of the complicated things under the hood.
- BUT, what if you've popped that screen from the widget tree and then again visit this page? Because in the above case we're only going to another tab without popping the current page.
- In popped screen case, you'll not be able to get the previous position of the scroll view, because when you pop the screen flutter will also remove the
PageStorageKey
attached to it. - To solve this issue we need to wrap
PageStorage
inside the parent widget of the widget tree. In our case, we can wrap it inside theScaffold
because theroute
is created before the build final globalBucket = PageStorageBucket(); '''Don't declare it inside any class. Declare it on global level.'''
Widget build(BuildContext context) { return PageStorage( bucket: globalBucket, child: Scaffold( bottomNavigationBar: BottomNavigationBar( backgroundColor: Theme.of(context).primaryColor, selectedItemColor: Colors.white, unselectedItemColor: Colors.white70, currentIndex: index, items: [ BottomNavigationBarItem( icon: Icon(Icons.list), title: Text('ListView'), ), BottomNavigationBarItem( icon: Icon(Icons.person), title: Text('Blah blah'), ), ], onTap: (int index) => setState(() => this.index = index), ), appBar: AppBar(), body: buildPages(), ), ); }
- Now it is working
GlobalKey
- This is the most used Key in Flutter compare to the above Keys.
- The
GlobalKey
can be used to change the parents anywhere in your app without losing state - It can be used to access information about another widget when we are on a completely different location of the widget tree.
- The common use case of the
GlobalKey
is validating aForm
or displaying theSnackbar
in the app etc. - Let's take an example.
final _counterState = GlobalKey<_CounterState>(); //Declaring the GlobalKey of CounterState
Scaffold(
appBar: AppBar(),
body: SizedBox.expand(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Counter(
key: _counterState,
),
],
),
),
);
class Counter extends StatefulWidget { const Counter({ Key? key, }) : super(key: key); @override _CounterState createState() => _CounterState(); }
class _CounterState extends State<Counter> { late int count; @override void initState() { super.initState(); count = 0; } @override Widget build(BuildContext context) { return Column( children: <Widget>[ Text( count.toString(), style: TextStyle(fontSize: 30), ), ElevatedButton( onPressed: () { setState(() { count++; }); }, child: Text("Add")) ], ); } }
- Output
- Now we can access the
count
value ofCounterWidget
in any page by passing theGlobalKey
class SecondPage extends StatefulWidget { final GlobalKey<_CounterState> counterKey; SecondPage(this.counterKey); @override _SecondPageState createState() => _SecondPageState(); }
class _SecondPageState extends State<SecondPage> { @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++; //here print(widget.counterKey.currentState!.count); }); }, ), Text( widget.counterKey.currentState!.count.toString(), style: TextStyle(fontSize: 50), ), ], ), ), ); } }
- So as we can see GlobalKey can be used to access information about another widget when we are on a completely different location of the widget tree.
THAT'S IT
- That's all you need to know about Keys.
- Hope you liked it. Thanks for reading
Feedback and Comments are welcomed 🙂