Mastering ValueKey, ObjectKey, and GlobalKey in Flutter
Mastering ValueKey, ObjectKey, and GlobalKey in Flutter
What You’ll Learn
Keys are Flutter’s way of preserving widget state when the widget tree changes. When widgets move around in a list or get reordered, Flutter needs keys to identify which widget is which and maintain their state correctly. Without keys, you might lose user input, scroll position, or animation state unexpectedly.
Understanding Different Key Types
Flutter provides several types of keys, each suited for different scenarios:
ValueKey - Uses a simple value (String, int, etc.) to identify widgets ObjectKey - Uses an entire object for identification GlobalKey - Provides access to the widget’s State from anywhere in the app UniqueKey - Generates a unique key every time (rarely needed)
Example: The Problem Without Keys
class TodoList extends StatefulWidget {
@override
State<TodoList> createState() => _TodoListState();
}
class _TodoListState extends State<TodoList> {
List<String> todos = ['Buy milk', 'Walk dog', 'Write code'];
void removeTodo(int index) {
setState(() {
todos.removeAt(index);
});
}
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: todos.length,
itemBuilder: (context, index) {
return TodoItem(
text: todos[index],
onDelete: () => removeTodo(index),
);
},
);
}
}
class TodoItem extends StatefulWidget {
final String text;
final VoidCallback onDelete;
const TodoItem({required this.text, required this.onDelete});
@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!),
),
title: Text(widget.text),
trailing: IconButton(
icon: Icon(Icons.delete),
onPressed: widget.onDelete,
),
);
}
}
In this example, if you check a todo and then delete a different todo above it, the wrong checkbox might appear checked. This happens because Flutter reuses the State objects without keys.
Solution: Using ValueKey
// In the ListView.builder:
itemBuilder: (context, index) {
return TodoItem(
key: ValueKey(todos[index]), // Add this line
text: todos[index],
onDelete: () => removeTodo(index),
);
}
Now Flutter uses the todo text as a unique identifier to match State objects correctly.
When to Use Each Key Type
ValueKey - When you have simple unique values (IDs, strings)
ValueKey<int>(user.id)
ValueKey<String>('unique_identifier')
ObjectKey - When the entire object serves as the identifier
ObjectKey(todoItem) // Uses the object instance
GlobalKey - When you need to access State or context from outside the widget tree
final formKey = GlobalKey<FormState>();
// Later, from anywhere:
if (formKey.currentState!.validate()) {
// Form is valid
}
Try It Yourself
Create a reorderable list of items with checkboxes. Try removing items with and without keys to see the difference in behavior. Add a drag-and-drop feature using ReorderableListView and notice how keys preserve the checked state during reordering.
ReorderableListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return TodoItem(
key: ValueKey(items[index].id), // Essential for reordering!
item: items[index],
);
},
onReorder: (oldIndex, newIndex) {
// Handle reordering logic
},
)
Tip of the Day
GlobalKeys are powerful but expensive - each GlobalKey maintains state and takes up memory. Use them sparingly, only when you truly need to access widget state from outside the widget tree. For most list scenarios, ValueKey or ObjectKey is sufficient and more performant. Also, never create new Key objects inside the build method - define them as final variables or use stable identifiers like IDs from your data model.