What is keys in flutter and when to use key in flutter ?
Answer
Keys in Flutter
Keys are identifiers for widgets, elements, and semantic nodes in Flutter. They help Flutter identify which widgets have changed, been added, or removed when the widget tree is rebuilt. Keys are essential for maintaining state and optimizing widget updates.
When to Use Keys
| Scenario | Need Key? | Reason |
|---|---|---|
| Stateless widgets (no reordering) | ❌ No | Flutter can track by position |
| Stateful widgets (reordering) | ✅ Yes | Need to preserve state |
| List items being reordered | ✅ Yes | Maintain identity during reorder |
| Swapping widget positions | ✅ Yes | Distinguish between widgets |
| Modifying collection (add/remove) | ✅ Yes | Track individual items |
| Static widget tree | ❌ No | No changes to track |
Types of Keys
1. ValueKey
Uses a specific value as the key identifier. Best for simple, immutable values.
dart// Example: Todo list with ValueKey class TodoList extends StatelessWidget { final List<String> todos = ['Buy milk', 'Walk dog', 'Write code']; Widget build(BuildContext context) { return ListView( children: todos.map((todo) { return ListTile( key: ValueKey(todo), // Uses todo text as key title: Text(todo), ); }).toList(), ); } }
2. ObjectKey
Uses an object's identity as the key. Best when you have complex objects with unique identities.
dartclass User { final int id; final String name; User(this.id, this.name); } class UserList extends StatelessWidget { final List<User> users = [ User(1, 'Alice'), User(2, 'Bob'), User(3, 'Charlie'), ]; Widget build(BuildContext context) { return ListView( children: users.map((user) { return ListTile( key: ObjectKey(user), // Uses user object as key title: Text(user.name), ); }).toList(), ); } }
3. UniqueKey
Generates a unique key that never equals another key. Best when you need guaranteed uniqueness but don't have a natural identifier.
dartclass DynamicList extends StatefulWidget { _DynamicListState createState() => _DynamicListState(); } class _DynamicListState extends State<DynamicList> { List<Widget> items = []; void addItem() { setState(() { items.add( Container( key: UniqueKey(), // Each widget gets unique key height: 50, color: Colors.blue, ), ); }); } Widget build(BuildContext context) { return Column(children: items); } }
4. GlobalKey
Allows you to access the widget's state from anywhere in the app. Use sparingly as it can create tight coupling.
dartclass FormExample extends StatelessWidget { final GlobalKey<FormState> _formKey = GlobalKey<FormState>(); Widget build(BuildContext context) { return Form( key: _formKey, // GlobalKey for form child: Column( children: [ TextFormField( validator: (value) { if (value == null || value.isEmpty) { return 'Please enter text'; } return null; }, ), ElevatedButton( onPressed: () { // Access form state from anywhere using global key if (_formKey.currentState!.validate()) { print('Form is valid!'); } }, child: Text('Submit'), ), ], ), ); } }
5. PageStorageKey
Preserves scroll position and other page state when navigating away and back.
dartclass TabViewExample extends StatelessWidget { Widget build(BuildContext context) { return DefaultTabController( length: 2, child: Scaffold( appBar: AppBar( bottom: TabBar( tabs: [Tab(text: 'Tab 1'), Tab(text: 'Tab 2')], ), ), body: TabBarView( children: [ ListView.builder( key: PageStorageKey('tab1'), // Preserves scroll position itemCount: 100, itemBuilder: (context, index) => ListTile(title: Text('Item $index')), ), ListView.builder( key: PageStorageKey('tab2'), // Preserves scroll position itemCount: 100, itemBuilder: (context, index) => ListTile(title: Text('Item $index')), ), ], ), ), ); } }
Practical Example: Reorderable List with Keys
Without Keys (Broken)
dartclass CounterTile extends StatefulWidget { _CounterTileState createState() => _CounterTileState(); } class _CounterTileState extends State<CounterTile> { int count = 0; Widget build(BuildContext context) { return ListTile( title: Text('Count: $count'), trailing: IconButton( icon: Icon(Icons.add), onPressed: () => setState(() => count++), ), ); } } class BrokenList extends StatefulWidget { _BrokenListState createState() => _BrokenListState(); } class _BrokenListState extends State<BrokenList> { List<CounterTile> items = [CounterTile(), CounterTile()]; void swap() { setState(() { final temp = items[0]; items[0] = items[1]; items[1] = temp; }); } Widget build(BuildContext context) { return Column( children: [ ...items, ElevatedButton( onPressed: swap, child: Text('Swap'), ), ], ); } } // Problem: Swapping doesn't preserve individual counter states
With Keys (Fixed)
dartclass FixedList extends StatefulWidget { _FixedListState createState() => _FixedListState(); } class _FixedListState extends State<FixedList> { List<CounterTile> items = [ CounterTile(key: UniqueKey()), CounterTile(key: UniqueKey()), ]; void swap() { setState(() { final temp = items[0]; items[0] = items[1]; items[1] = temp; }); } Widget build(BuildContext context) { return Column( children: [ ...items, ElevatedButton( onPressed: swap, child: Text('Swap'), ), ], ); } } // Solution: Keys preserve state during reordering
How Keys Work Internally
When Flutter rebuilds the widget tree:
- Without Keys: Matches widgets by type and position
- With Keys: Matches widgets by type and key
dart// Example: Flutter's matching algorithm // Initial build Row( children: [ Container(color: Colors.red), // Position 0 Container(color: Colors.blue), // Position 1 ], ) // After rebuild (without keys) Row( children: [ Container(color: Colors.blue), // Matches position 0 (WRONG!) Container(color: Colors.red), // Matches position 1 (WRONG!) ], ) // After rebuild (with keys) Row( children: [ Container(key: ValueKey('blue'), color: Colors.blue), // Matches by key ✅ Container(key: ValueKey('red'), color: Colors.red), // Matches by key ✅ ], )
Key Selection Guide
| Use Case | Recommended Key Type | Example |
|---|---|---|
| Simple values (String, int) | text | text |
| Complex objects | text | text |
| Always unique items | text | text |
| Access state globally | text | text |
| Preserve scroll position | text | text |
Best Practices
- Use Keys Sparingly: Only when needed for correctness
- Choose the Right Key Type: Match the key type to your use case
- Avoid GlobalKey Unless Necessary: It creates tight coupling
- Keys Must Be Unique Among Siblings: Not globally unique (except UniqueKey)
- Keys Should Be Stable: Don't generate new keys on every build
dart// ❌ Bad: Generating new key on every build Widget build(BuildContext context) { return ListTile( key: UniqueKey(), // New key every rebuild! title: Text('Item'), ); } // ✅ Good: Stable key final itemKey = UniqueKey(); // Created once Widget build(BuildContext context) { return ListTile( key: itemKey, // Same key across rebuilds title: Text('Item'), ); }
Common Pitfalls
dart// ❌ Bad: Using index as key in reorderable list ListView.builder( itemBuilder: (context, index) { return ListTile( key: ValueKey(index), // Index changes when items move! title: Text(items[index]), ); }, ); // ✅ Good: Using item ID as key ListView.builder( itemBuilder: (context, index) { return ListTile( key: ValueKey(items[index].id), // ID stays with item title: Text(items[index].name), ); }, );
Debugging Keys
Enable key debugging to see how Flutter uses keys:
dartvoid main() { debugPrintGlobalKeyedWidgetLifecycle = true; runApp(MyApp()); }
Important: Keys are Flutter's way of maintaining widget identity. Use them when widget order changes, but don't overuse them as they add overhead.
Documentation: Flutter Keys Documentation