Every Flutter developer, be it a beginner or an experienced one, must have encountered the term Flutter key. Have you ever wondered what role keys play in building robust and efficient Flutter applications? If yes, then stick around as we are going to delve into the profound aspects of Flutter keys.
In this blog post, you'll gain a solid understanding and practical knowledge about the different types of keys, including Flutter unique key, Flutter widget key, and Flutter global key, along with their usage in building modern and efficient Flutter apps.
The concept of keys in Flutter is omnipresent; the term 'key' often hits us when we face problems with our widgets during state management, navigation, or while formulating the element tree. The notion of 'keys' becomes crucial when we have to deal with 'stateful widgets' or a 'widget tree' that shares the same parent.
To understand the true power of keys, we need to start from scratch. Keys in Flutter are an optional parameter in the basic structure of a widget constructor. Basically, every widget constructor in Flutter includes a key parameter that has a crucial role in the widget's lifecycle.
1 Widget({ 2 Key? key, 3 ... 4 }) : super(key: key); 5
This generic type of Flutter key can further be extended as a Flutter widget key, which happens to be a unique identifier for the widgets, elements, and semantics nodes. Its purpose? As the widget tree gets updated, the key ensures that the state in your widget tree stays in sync with how widgets get rebuilt.
When we talk about a Flutter key, the first thing that comes to mind is that it uniquely identifies a widget in a widget tree. But what does it mean to uniquely identify widgets? And why do keys need to be unique?
Consider a widget tree with multiple widgets of the same type. The Flutter framework needs a way to identify which widget correlates to what kind of information and state. Here, the role of a Flutter key comes into play. A key uniquely identifies a widget amongst its siblings in the widget tree.
But it's vital to remember that keys must be unique only among the direct siblings in the widget tree. Two widgets under different parents can have the same key without causing any issues. Here comes a code snippet as an example:
1 2 Column( 3 children: <Widget>[ 4 Padding( 5 padding: const EdgeInsets.all(8.0), 6 child: FlutterLogo(key: Key('logo'), size: 60), 7 ), 8 Padding( 9 padding: const EdgeInsets.all(8.0), 10 child: FlutterLogo(key: Key('logo'), size: 60), 11 ), 12 ], 13 ) 14 15
In the above case, a 'unique key' is assigned to each FlutterLogo widget. Each 'FlutterLogo' in the 'Column' can be uniquely identified using the logo key.
One of the essential characteristics of keys in Flutter is that they are immutable. But why does Flutter strictly enforce this 'immutable'-ness on keys? As per the rules of the Dart language, which Flutter is based upon, the equality of an object is determined by its properties. If we change a property of an object key, this would naturally lead to its equality being disturbed, inherently leading to the mismatch of widgets, causing improper rendering and defeating the very purpose of having keys.
This concept also extends, albeit indirectly, to the stateful widgets as well. Although the state within stateful widgets can change, the key associated with a stateful widget itself is immutable. This characteristic prevents complications in updating and matching the widgets in the widget tree effectively, leading to a smoother user interface experience.
1 2 class ExampleWidget extends StatefulWidget { 3 ExampleWidget({Key key}) : super(key: key); 4 5 @override 6 _ExampleWidgetState createState() => _ExampleWidgetState(); 7 } 8 9 class _ExampleWidgetState extends State<ExampleWidget> { 10 // The state of your widget... 11 } 12 13
In the above code, although the _ExampleWidgetState might mutate over time, the key associated with ExampleWidget remains immutable, thus ensuring the correct identification and operation of the widget.
The gears of Flutter work tirelessly to maintain a smooth and reactive user interface. But did you ever wonder what empowers Flutter to 'update' appropriately and render an efficient UI experience? That's right! It’s our valuable Flutter keys.
To support this operability end-to-end, Flutter maintains two trees: the Element Tree and the Widget Tree. With each rebuild, these trees go through the process of widget matching, where Flutter checks whether the new widget from the same parent, built from the same location in your code, matches the type and key of the previous widget.
1 2 Widget build(BuildContext context) { 3 return Container( 4 child: Column( 5 children: <Widget>[ 6 Text('Hello, Flutter!', key: Key('Text1')), 7 Text('I love Flutter!', key: Key('Text2')), 8 ], 9 ), 10 ); 11 } 12 13
In the example above, Flutter checks if the Text widget with the key Text1 and the Text widget with the key Text2 are the same as they were during the previous build process. The keys, 'Text1' and 'Text2', help Flutter ensure that these widgets preserve their states across multiple builds.
A widget is simply a description of the UI. But when a Flutter app is built, it does more than just draw widgets. It also updates them over the lifecycle of the app. What is the secret that Flutter uses to unfailingly map the right data to the corresponding widget after the tree’s reconstruction? This secret is none other than the incredible relationship that a Widget shares with its Key.
Now, how does this interaction between a widget and its key occur? Upon the rebuild process, Flutter compares the new widget with the previous widget. It checks both the type and the keys. If they match, the framework updates the existing widget with the new widgets.
1 2 Widget build(BuildContext context) { 3 return Container( 4 child: Column( 5 children: <Widget>[ 6 FlutterLogo(key: ValueKey(1)), 7 Text('Hello Flutter!', key: ValueKey(2)), 8 ], 9 ), 10 ); 11 } 12 13
In the example above, even though the Tree gets rebuilt, the FlutterLogo and Text widgets maintain their state and positions because their respective keys match with those in the previous widget tree. This matching process is central to maintaining consistency inside your app's UI.
In Flutter, Keys are not restricted to being simple strings or integers. They have subclasses that open up a plethora of possibilities in managing and controlling the state across the entire widget subtree. The two primary subclasses of key we're going to focus on, are GlobalKey and LocalKey.
The GlobalKey, as the name suggests, is not restricted to identifying widgets in the local scope. A GlobalKey maintains the state of a widget across the entire widget tree of the application. This offers a distinct advantage, particularly when the stateful widget needs to be preserved, even after getting moved around in the widget tree.
GlobalKey proves itself to be of great help during form validation in Flutter app, enforcing uniqueness across the entire widget tree and maintaining the previous state. To store the form's state in a GlobalKey, this piece of code could be implemented:
1 2 final _formKey = GlobalKey<FormState>(); 3 4 @override 5 Widget build(BuildContext context) { 6 return Form( 7 key: _formKey, 8 child: Column( 9 children: <Widget>[ 10 TextFormField( 11 validator: (value) { 12 if (value.isEmpty) { 13 return 'Please enter some text'; 14 } 15 return null; 16 }, 17 ), 18 ElevatedButton( 19 onPressed: () { 20 if (_formKey.currentState.validate()) { 21 ScaffoldMessenger.of(context).showSnackBar( 22 const SnackBar(content: Text('Processing Data')), 23 ); 24 } 25 }, 26 child: const Text('Submit'), 27 ), 28 ], 29 ), 30 ); 31 } 32
A GlobalKey helps identify the Form from any part of the widget tree and appropriately validate it. This eases the process of form validation significantly, especially in large applications.
While a GlobalKey may seem like the all-powerful tool, most developers shy away from using it. Why, you ask? GlobalKeys are more resource-intensive, as they enforce uniqueness across the entire widget tree, rather than a part of it. This is where the LocalKey comes into the picture. It operates within a local context and perfectly suits most scenarios where identifier uniqueness is primarily required in the immediate vicinity of the widget tree only.
Here is a basic example of how to use a LocalKey with a list of widgets. In the following snippet, we used a ValueKey, which is a LocalKey that uses a value to determine the equality.
1 2 ListView( 3 children: <Widget>[ 4 ListTile( 5 leading: Icon(Icons.map), 6 title: Text('Map'), 7 key: ValueKey('list_tile_1'), 8 ), 9 ListTile( 10 leading: Icon(Icons.photo_album), 11 title: Text('Album'), 12 key: ValueKey('list_tile_2'), 13 ), 14 ListTile( 15 leading: Icon(Icons.phone), 16 title: Text('Phone'), 17 key: ValueKey('list_tile_3'), 18 ), 19 ], 20 ) 21 22
Here, a unique 'ValueKey' is assigned to each ListTile widget within the ListView. This allows Flutter to maintain the state and identity of these widgets even as the ListView rebuilds.
Just as in the case of most classes, Flutter Keys provide constructors to create instances of keys. In the world of Flutter Keys, we often encounter two constructors: Key(String value) and Key.empty(). But the mystery remains - what is the difference between these two, and when are they used? Let's delve into these constructors one by one.
Key(String value) is a factory constructor that creates keys with a string. Technically, it constructs a ValueKey<String>
, which is a LocalKey, with the given value. The value provides the object's identity. The primary purpose of using this constructor is assigning a particular value to a widget to maintain their identity during the rebuild process.
1 2 Widget build(BuildContext context) { 3 return ListView( 4 children: <Widget>[ 5 ListTile( 6 leading: Icon(Icons.map), 7 title: Text('Map'), 8 key: Key('list_tile_1'), 9 ), 10 ListTile( 11 leading: Icon(Icons.photo_album), 12 title: Text('Album'), 13 key: Key('list_tile_2'), 14 ), 15 ], 16 ); 17 } 18 19
In the above example, we assign a unique key to each ListTile widget. This allows Flutter to maintain the widget's state even when the ListView rebuilds.
On the other hand, Key.empty() is the default constructor that reserves a place for subclasses. This constructor isn't typically used directly but rather leveraged when creating your own custom subclasses of Keys.
1 2 class MyKey extends LocalKey { 3 MyKey.empty() : super.empty(); 4 } 5 6
In the example above, we create a custom key, 'MyKey', subclassing 'LocalKey', and use the Key.empty() constructor.
In this section, we'll shed light on two core methods associated with keys: noSuchMethod(Invocation invocation) and toString(). These methods allow the framework to interact with keys intuitively and are inherited from the [Object] class, which forms the root class of Dart's class hierarchy.
The noSuchMethod() is invoked when a non-existent method or property is called on a key. This mechanism is essentially the Dart language's way of providing 'last-minute defense in case of hiccups.
While it's very rare for a Dart program to explicitly call this method, it nevertheless plays an essential role in Dart's exception mechanism related to non-existent methods.
The toString() method simply provides you with the string representation of the Key object. In the context of debugging or logging, this method proves invaluable as it provides insight into the specific Key constructed, improving the information available when debugging.
Here's an example of a GlobalKey's toString() method:
If you run the above code, it would print something like this:
1 [GlobalKey#4c69a] 2
The hexadecimal id following the hashtag (#) differentiates one key from another, facilitating their identification and ensuring uniqueness.
Key properties are integral components that aid in better understanding and managing a key's attributes during runtime. Among the many properties of a Flutter Key, two stand out as particularly significant: hashCode and runtimeType.
In Dart, every object has an hashCode, which is an integer representing the object. Immutable objects that are logically equal should return the same hashCode. This hashCode is used to quickly lookup objects in a collection like a HashSet or a HashMap. For instance, when we add a key to a multitude of keys, Flutter uses the hashCode property to manage these keys in hash collections, making it easier and faster to search, retrieve or compare keys.
The runtimeType is a property inherited from the Object class in Dart, and it returns the runtime type of the object. This can be helpful for understanding the type of Key we're working with during runtime. It primarily serves debugging purposes and isn't intended for usage in production code.
Here's a quick peek at these properties in action:
1 2 final GlobalKey key1 = GlobalKey(); 3 final ValueKey key2 = ValueKey(1); 4 5 print('Global key hash code: ${key1.hashCode}'); 6 print('Global key runtime type: ${key1.runtimeType}'); 7 8 print('Value key hash code: ${key2.hashCode}'); 9 print('Value key runtime type: ${key2.runtimeType}'); 10 11
As we wrap up our deep-dive into Flutter keys, one discernible facet becomes evident: keys are an integral part of the Flutter framework that have robust mechanisms allowing developers to utilize them efficiently. This is what empowers Flutter to develop interactive interfaces with terrific speed.
Keys play a pivotal role in determining the identity of widgets and making sense of the widget tree. They enable you to handle widgets, handle their states, and manage re-renders effectively. From ensuring the uniqueness among siblings in the widget tree via LocalKey to preserving and handling the state across the entire application with GlobalKey, keys exhibit it all.
Above all, keys make form validation and state management in a Flutter app less of a chore, and more of an exercise in careful alignment with Flutter's intuitive paradigm. By understanding the foundation of Flutter keys, you've now unlocked a powerful tool to enhance your Flutter applications and optimize for a performant user interface!
Whether you're climbing the ladder of learning Flutter, or are already up in the rungs, we hope that you've found this exploration of Flutter keys helpful. Key management is becoming progressively more critical in today's app development world to maintain widget states and improve overall performance.
Tired of manually designing screens, coding on weekends, and technical debt? Let DhiWise handle it for you!
You can build an e-commerce store, healthcare app, portfolio, blogging website, social media or admin panel right away. Use our library of 40+ pre-built free templates to create your first application using DhiWise.