Using Keys in Flutter: When and Why
Using Keys in Flutter: When and Why
What You’ll Learn
Keys are a powerful but often misunderstood concept in Flutter. You’ll learn when Flutter needs keys to preserve widget state, which key types to use in different situations, and how to avoid common pitfalls when working with dynamic widget lists.
Why Keys Matter
Flutter uses keys to identify widgets when the widget tree is rebuilt. Without keys, Flutter relies on the widget’s type and position to determine if a widget can be reused. This works fine for static lists, but causes problems with stateful widgets in dynamic lists.
Consider this scenario: you have a list of todo items, each with a checkbox. When you reorder or remove items, Flutter might reuse the wrong checkbox state because it only looks at position, not identity.
Types of Keys
ValueKey: Use when widgets have a unique value (like an ID or name)
ListView(
children: items.map((item) =>
TodoItem(
key: ValueKey(item.id),
title: item.title,
)
).toList(),
)
ObjectKey: Use when the entire object is unique
ListTile(
key: ObjectKey(user),
title: Text(user.name),
)
UniqueKey: Generates a unique key, use when no other unique identifier exists
Container(
key: UniqueKey(), // Creates new widget every rebuild
child: RandomColorBox(),
)
GlobalKey: Access widget state from anywhere (use sparingly - it’s expensive)
final formKey = GlobalKey<FormState>();
Form(
key: formKey,
child: Column(children: [...]),
)
// Later in code:
if (formKey.currentState!.validate()) {
formKey.currentState!.save();
}
Example: Stateful List Without Keys (Broken)
class BrokenList extends StatefulWidget {
@override
State<BrokenList> createState() => _BrokenListState();
}
class _BrokenListState extends State<BrokenList> {
List<String> items = ['Apple', 'Banana', 'Cherry'];
@override
Widget build(BuildContext context) {
return ListView(
children: items.map((item) =>
StatefulTile(title: item) // No key!
).toList(),
);
}
}
class StatefulTile extends StatefulWidget {
final String title;
const StatefulTile({required this.title});
@override
State<StatefulTile> createState() => _StatefulTileState();
}
class _StatefulTileState extends State<StatefulTile> {
bool isChecked = false;
@override
Widget build(BuildContext context) {
return CheckboxListTile(
title: Text(widget.title),
value: isChecked,
onChanged: (val) => setState(() => isChecked = val!),
);
}
}
Problem: If you remove “Banana” after checking its checkbox, “Cherry” will appear checked because Flutter reuses the state from position 2.
Example: Fixed with Keys
class FixedList extends StatefulWidget {
@override
State<FixedList> createState() => _FixedListState();
}
class _FixedListState extends State<FixedList> {
List<String> items = ['Apple', 'Banana', 'Cherry'];
@override
Widget build(BuildContext context) {
return ListView(
children: items.map((item) =>
StatefulTile(
key: ValueKey(item), // Added key!
title: item,
)
).toList(),
);
}
}
Now Flutter tracks each tile by its unique value, preserving the correct state even when the list changes.
Try It Yourself
Create a simple reorderable list of colored containers with checkboxes:
- Make each container a stateful widget with a checkbox
- First, implement without keys and try removing items after checking some
- Then add ValueKey or ObjectKey and observe the difference
- Try moving items around - does state follow the item or the position?
Tip of the Day
When to use keys: Only use keys when you have multiple widgets of the same type with state that needs to be preserved. For stateless widgets or static lists, keys add unnecessary overhead. Common use cases: reorderable lists, lists where items can be inserted/removed, and animated transitions.
Common mistake: Using UniqueKey() directly in the build method creates a NEW key every rebuild, causing Flutter to destroy and recreate the widget every time. If you need UniqueKey, create it once in the state, not in build().