Understanding Keys in Flutter: When and Why to Use Them
Understanding Keys in Flutter: When and Why to Use Them
What You’ll Learn
Learn how Flutter Keys help identify widgets uniquely and preserve state when the widget tree changes. Master when to use ValueKey, ObjectKey, UniqueKey, and GlobalKey to solve common Flutter bugs.
The Widget Identity Problem
Flutter’s widget tree is constantly rebuilt. Sometimes widgets move positions or get reordered, and Flutter needs to know which widget is which to preserve state properly. That’s where Keys come in.
When do you need Keys?
- Reordering lists or grids
- Removing/adding items from collections
- Swapping widget positions
- Preserving state across rebuilds
- Accessing widgets from outside their tree
Example: The Swapping Bug
Let’s see the problem Keys solve:
import 'package:flutter/material.dart';
class NoKeyExample extends StatefulWidget {
@override
State<NoKeyExample> createState() => _NoKeyExampleState();
}
class _NoKeyExampleState extends State<NoKeyExample> {
List<Widget> tiles = [
ColorTile(color: Colors.red),
ColorTile(color: Colors.blue),
];
void _swapTiles() {
setState(() {
tiles.insert(1, tiles.removeAt(0));
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Without Keys')),
body: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: tiles,
),
SizedBox(height: 20),
ElevatedButton(
onPressed: _swapTiles,
child: Text('Swap Tiles'),
),
],
),
);
}
}
class ColorTile extends StatefulWidget {
final Color color;
const ColorTile({Key? key, required this.color}) : super(key: key);
@override
State<ColorTile> createState() => _ColorTileState();
}
class _ColorTileState extends State<ColorTile> {
@override
Widget build(BuildContext context) {
return Container(
width: 100,
height: 100,
color: widget.color,
margin: EdgeInsets.all(8),
);
}
}
The bug: When you tap “Swap Tiles”, the colors don’t swap! This happens because Flutter reuses the existing State objects based on widget type and position.
Solution: Add Keys
class WithKeyExample extends StatefulWidget {
@override
State<WithKeyExample> createState() => _WithKeyExampleState();
}
class _WithKeyExampleState extends State<WithKeyExample> {
List<Widget> tiles = [
ColorTile(key: UniqueKey(), color: Colors.red),
ColorTile(key: UniqueKey(), color: Colors.blue),
];
void _swapTiles() {
setState(() {
tiles.insert(1, tiles.removeAt(0));
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('With Keys')),
body: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: tiles,
),
SizedBox(height: 20),
ElevatedButton(
onPressed: _swapTiles,
child: Text('Swap Tiles'),
),
],
),
);
}
}
Now the colors swap correctly! Keys tell Flutter which widget is which.
Types of Keys
1. ValueKey
Use when you have a unique value to identify the widget:
ListView(
children: items.map((item) {
return ListTile(
key: ValueKey(item.id), // item.id is unique
title: Text(item.name),
);
}).toList(),
)
2. ObjectKey
Use when your data object is unique:
class Person {
final String name;
final int age;
Person(this.name, this.age);
}
final people = [Person('Alice', 30), Person('Bob', 25)];
ListView(
children: people.map((person) {
return ListTile(
key: ObjectKey(person), // person object is unique
title: Text(person.name),
);
}).toList(),
)
3. UniqueKey
Use when you need a guaranteed unique key:
// Creates a new unique key every time
final tile = ColorTile(key: UniqueKey(), color: Colors.red);
Warning: Don’t create UniqueKey in build method - it generates a new key on every rebuild!
// WRONG - creates new key on every rebuild
Widget build(BuildContext context) {
return MyWidget(key: UniqueKey());
}
// RIGHT - create once and reuse
final myKey = UniqueKey();
Widget build(BuildContext context) {
return MyWidget(key: myKey);
}
4. GlobalKey
Use when you need to access widget state or context from anywhere:
class FormExample extends StatelessWidget {
final _formKey = GlobalKey<FormState>();
@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Column(
children: [
TextFormField(
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter some text';
}
return null;
},
),
ElevatedButton(
onPressed: () {
// Access form state from outside
if (_formKey.currentState!.validate()) {
print('Form is valid!');
}
},
child: Text('Submit'),
),
],
),
);
}
}
Practical Example: Reorderable Todo List
class TodoListScreen extends StatefulWidget {
@override
State<TodoListScreen> createState() => _TodoListScreenState();
}
class _TodoListScreenState extends State<TodoListScreen> {
List<Todo> todos = [
Todo(id: '1', title: 'Learn Flutter Keys'),
Todo(id: '2', title: 'Build an app'),
Todo(id: '3', title: 'Deploy to production'),
];
void _removeTodo(String id) {
setState(() {
todos.removeWhere((todo) => todo.id == id);
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Todo List')),
body: ListView.builder(
itemCount: todos.length,
itemBuilder: (context, index) {
final todo = todos[index];
return TodoItem(
key: ValueKey(todo.id), // Preserve state when list changes
todo: todo,
onDelete: () => _removeTodo(todo.id),
);
},
),
);
}
}
class Todo {
final String id;
final String title;
Todo({required this.id, required this.title});
}
class TodoItem extends StatefulWidget {
final Todo todo;
final VoidCallback onDelete;
const TodoItem({
Key? key,
required this.todo,
required this.onDelete,
}) : super(key: key);
@override
State<TodoItem> createState() => _TodoItemState();
}
class _TodoItemState extends State<TodoItem> {
bool _isChecked = false;
@override
Widget build(BuildContext context) {
return ListTile(
leading: Checkbox(
value: _isChecked,
onChanged: (value) {
setState(() {
_isChecked = value ?? false;
});
},
),
title: Text(
widget.todo.title,
style: TextStyle(
decoration: _isChecked ? TextDecoration.lineThrough : null,
),
),
trailing: IconButton(
icon: Icon(Icons.delete),
onPressed: widget.onDelete,
),
);
}
}
Without the ValueKey, removing items would cause checkboxes to stay in wrong positions!
Try It Yourself
- Create a list of colored boxes that can be reordered by dragging
- Add a shuffle button that randomizes the order
- Give each box an internal counter - verify the counter stays with the correct box when reordering
- Try removing the keys and observe the bugs
Hint: Use ReorderableListView for drag-to-reorder:
ReorderableListView(
onReorder: (oldIndex, newIndex) {
// Handle reordering
},
children: items.map((item) {
return MyWidget(key: ValueKey(item.id), item: item);
}).toList(),
)
Tip of the Day
Key placement matters! Always attach keys to the top-level widget in your list/collection:
// WRONG - key on outer widget
Container(
key: ValueKey(item.id),
child: MyStatefulWidget(data: item),
)
// RIGHT - key on the stateful widget that needs preservation
Container(
child: MyStatefulWidget(
key: ValueKey(item.id),
data: item,
),
)
Performance tip: Use const keys when possible:
const ValueKey('my-unique-id') // Reuses same key instance
GlobalKey warning: GlobalKeys are expensive! Only use them when you truly need to access state from outside. For most cases, ValueKey or ObjectKey are sufficient and more performant.