Flutter Keys: When and How to Use Them

Flutter Keys: When and How to Use Them

What You’ll Learn

Keys are Flutter’s way of preserving state when widgets move around in the widget tree. Most of the time you don’t need them, but when you’re working with lists of stateful widgets that can be reordered or removed, keys become essential.

The Problem Keys Solve

Flutter identifies widgets by their position in the widget tree. When you have multiple widgets of the same type (like a list of items), Flutter can get confused about which widget is which when they’re reordered or removed.

Without keys, Flutter matches widgets by type and position. If you remove the first item from a list, Flutter thinks the first widget just changed its data, not that a different widget should be in that position. This can lead to wrong state being shown or animations not working correctly.

Example

Here’s a practical example showing the difference:

class ColoredBox extends StatefulWidget {
  final Color color;
  const ColoredBox({Key? key, required this.color}) : super(key: key);

  @override
  State<ColoredBox> createState() => _ColoredBoxState();
}

class _ColoredBoxState extends State<ColoredBox> {
  bool isExpanded = false;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () => setState(() => isExpanded = !isExpanded),
      child: Container(
        width: isExpanded ? 200 : 100,
        height: 100,
        color: widget.color,
        margin: EdgeInsets.all(8),
      ),
    );
  }
}

// Usage without keys (problematic)
class ItemListBad extends StatefulWidget {
  @override
  State<ItemListBad> createState() => _ItemListBadState();
}

class _ItemListBadState extends State<ItemListBad> {
  List<Color> colors = [Colors.red, Colors.blue, Colors.green];

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        ...colors.map((color) => ColoredBox(color: color)),
        ElevatedButton(
          onPressed: () => setState(() => colors.removeAt(0)),
          child: Text('Remove First'),
        ),
      ],
    );
  }
}

// Usage WITH keys (correct)
class ItemListGood extends StatefulWidget {
  @override
  State<ItemListGood> createState() => _ItemListGoodState();
}

class _ItemListGoodState extends State<ItemListGood> {
  List<Color> colors = [Colors.red, Colors.blue, Colors.green];

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        ...colors.map((color) => ColoredBox(
          key: ValueKey(color),  // Add unique key here
          color: color,
        )),
        ElevatedButton(
          onPressed: () => setState(() => colors.removeAt(0)),
          child: Text('Remove First'),
        ),
      ],
    );
  }
}

Types of Keys

ValueKey: Use when each item has unique data (like an ID or value)

ValueKey(user.id)  // For list items with unique IDs

ObjectKey: Use when the whole object is unique

ObjectKey(user)  // When the entire object distinguishes the item

UniqueKey: Creates a key that’s only equal to itself

UniqueKey()  // Use sparingly - creates new key each build

GlobalKey: Allows access to widget state from anywhere (use rarely)

final formKey = GlobalKey<FormState>();

Try It Yourself

Create a simple to-do list where items can be checked off and deleted. First build it without keys and try expanding an item (add a tap handler that changes size), then delete a different item—you’ll see the wrong item stays expanded. Then add ValueKey or ObjectKey to each list item and see how it fixes the issue.

Tip of the Day

You only need keys when widgets of the same type can move around or be removed. If your list never changes order and items are only added at the end, you probably don’t need keys. When in doubt, run your app and see if state behaves correctly. If widgets “forget” their state or show the wrong state after list changes, that’s when you need keys.