Skip to content

Keys

Understand how Keys identify and preserve widgets in the widget tree.


What is it?

Keys are identifiers that Flutter uses to uniquely identify widgets in the widget tree. They help Flutter efficiently update the UI by distinguishing between widgets, especially when widgets are added, removed, or reordered. Keys preserve state and improve performance during widget tree updates.


Why does it exist?

Keys exist to:

  • Uniquely identify widgets in the widget tree
  • Preserve state when widgets are moved or reordered
  • Improve performance during widget updates
  • Enable efficient list and grid rendering
  • Support form validation and focus management
  • Access widgets globally via GlobalKey
  • Maintain scroll positions in lists

When to Use Keys

Keys are needed in specific scenarios.

// 1. Reordering widgets in a list
class ReorderList extends StatefulWidget {
  const ReorderList({super.key});

  @override
  State<ReorderList> createState() => _ReorderListState();
}

class _ReorderListState extends State<ReorderList> {
  List<String> _items = ['Apple', 'Banana', 'Orange'];

  void _reorder(int oldIndex, int newIndex) {
    setState(() {
      if (oldIndex < newIndex) {
        newIndex -= 1;
      }
      final item = _items.removeAt(oldIndex);
      _items.insert(newIndex, item);
    });
  }

  @override
  Widget build(BuildContext context) {
    return ReorderableListView(
      onReorder: _reorder,
      children: _items.map((item) {
        // Key preserves state when reordering
        return ListTile(
          key: ValueKey(item),
          title: Text(item),
        );
      }).toList(),
    );
  }
}

// 2. Adding/removing widgets
class AddRemoveList extends StatefulWidget {
  const AddRemoveList({super.key});

  @override
  State<AddRemoveList> createState() => _AddRemoveListState();
}

class _AddRemoveListState extends State<AddRemoveList> {
  List<Widget> _widgets = [];

  void _addWidget() {
    setState(() {
      _widgets.add(
        Container(
          key: UniqueKey(), // Unique key for each widget
          color: Colors.blue,
          height: 50,
          margin: const EdgeInsets.all(4),
        ),
      );
    });
  }

  void _removeWidget() {
    setState(() {
      if (_widgets.isNotEmpty) {
        _widgets.removeLast();
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Row(
          children: [
            ElevatedButton(
              onPressed: _addWidget,
              child: const Text('Add'),
            ),
            ElevatedButton(
              onPressed: _removeWidget,
              child: const Text('Remove'),
            ),
          ],
        ),
        ..._widgets,
      ],
    );
  }
}

// 3. Preserving state across widget moves
class PreserveState extends StatefulWidget {
  const PreserveState({super.key});

  @override
  State<PreserveState> createState() => _PreserveStateState();
}

class _PreserveStateState extends State<PreserveState> {
  bool _showFirst = true;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        // Without key - state lost
        if (_showFirst)
          const CounterWithoutKey()
        else
          const CounterWithoutKey(),

        // With key - state preserved
        if (_showFirst)
          const CounterWithKey(key: ValueKey('counter'))
        else
          const CounterWithKey(key: ValueKey('counter')),

        ElevatedButton(
          onPressed: () {
            setState(() {
              _showFirst = !_showFirst;
            });
          },
          child: Text(_showFirst ? 'Show Second' : 'Show First'),
        ),
      ],
    );
  }
}

class CounterWithKey extends StatefulWidget {
  const CounterWithKey({super.key});

  @override
  State<CounterWithKey> createState() => _CounterWithKeyState();
}

class _CounterWithKeyState extends State<CounterWithKey> {
  int _count = 0;

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        Text('With Key: $_count'),
        ElevatedButton(
          onPressed: () {
            setState(() => _count++);
          },
          child: const Text('+'),
        ),
      ],
    );
  }
}

What's happening here? - Keys preserve state during reorder - Unique keys for dynamic widgets - Keys preserve state when widget type changes - Keys enable efficient updates


Key Types

Different key types serve different purposes.

// 1. ValueKey - Uses value for identity
class ValueKeyExample extends StatelessWidget {
  const ValueKeyExample({super.key});

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        // Uses the string value as identity
        Text(
          'Item 1',
          key: const ValueKey('item_1'),
        ),
        Text(
          'Item 2',
          key: const ValueKey('item_2'),
        ),
        // Uses integer value
        Text(
          'Item 3',
          key: ValueKey(3),
        ),
      ],
    );
  }
}

// 2. ObjectKey - Uses object identity
class Person {
  final String id;
  final String name;

  const Person(this.id, this.name);

  @override
  bool operator ==(Object other) {
    return other is Person && other.id == id;
  }

  @override
  int get hashCode => id.hashCode;
}

class ObjectKeyExample extends StatelessWidget {
  const ObjectKeyExample({super.key});

  @override
  Widget build(BuildContext context) {
    final person = Person('1', 'Alice');

    return Column(
      children: [
        // Uses object identity
        Text(
          person.name,
          key: ObjectKey(person),
        ),
      ],
    );
  }
}

// 3. UniqueKey - Always unique
class UniqueKeyExample extends StatelessWidget {
  const UniqueKeyExample({super.key});

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        // Each widget gets unique key
        Container(
          key: UniqueKey(),
          color: Colors.red,
          height: 50,
        ),
        Container(
          key: UniqueKey(),
          color: Colors.blue,
          height: 50,
        ),
      ],
    );
  }
}

// 4. GlobalKey - Accessible anywhere
class GlobalKeyExample extends StatefulWidget {
  const GlobalKeyExample({super.key});

  @override
  State<GlobalKeyExample> createState() => _GlobalKeyExampleState();
}

class _GlobalKeyExampleState extends State<GlobalKeyExample> {
  final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
  final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();

  void _validateForm() {
    // Access form state using GlobalKey
    if (_formKey.currentState?.validate() ?? false) {
      _formKey.currentState?.save();
      // Form is valid
    }
  }

  void _showSnackBar() {
    // Access scaffold state using GlobalKey
    _scaffoldKey.currentState?.showSnackBar(
      const SnackBar(content: Text('Hello')),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      key: _scaffoldKey,
      body: Form(
        key: _formKey,
        child: Column(
          children: [
            TextFormField(
              validator: (value) {
                if (value == null || value.isEmpty) {
                  return 'Required';
                }
                return null;
              },
            ),
            ElevatedButton(
              onPressed: _validateForm,
              child: const Text('Validate'),
            ),
            ElevatedButton(
              onPressed: _showSnackBar,
              child: const Text('Show SnackBar'),
            ),
          ],
        ),
      ),
    );
  }
}

// 5. PageStorageKey - Preserves scroll position
class PageStorageKeyExample extends StatefulWidget {
  const PageStorageKeyExample({super.key});

  @override
  State<PageStorageKeyExample> createState() => _PageStorageKeyExampleState();
}

class _PageStorageKeyExampleState extends State<PageStorageKeyExample> {
  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      // Preserves scroll position
      key: const PageStorageKey('my_list'),
      itemCount: 100,
      itemBuilder: (context, index) {
        return ListTile(
          title: Text('Item $index'),
        );
      },
    );
  }
}

What's happening here? - ValueKey: identifies by value - ObjectKey: identifies by object - UniqueKey: always unique - GlobalKey: accessible anywhere - PageStorageKey: preserves scroll


Key Comparison

Choosing the right key for your use case.

// Comparison of key types
class KeyComparison extends StatelessWidget {
  const KeyComparison({super.key});

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        // 1. ValueKey - Best for lists with unique IDs
        const Text(
          'ValueKey',
          key: ValueKey('text_1'),
        ),

        // 2. ObjectKey - Best for custom objects
        const Text(
          'ObjectKey',
          key: ObjectKey('text_2'),
        ),

        // 3. UniqueKey - Best for dynamic content
        const Text(
          'UniqueKey',
          key: UniqueKey(),
        ),

        // 4. GlobalKey - Best for form access
        // final GlobalKey _key = GlobalKey();
        // Text('GlobalKey', key: _key),

        // 5. PageStorageKey - Best for scroll positions
        // ListView(key: PageStorageKey('list')),
      ],
    );
  }
}

// When to use each key:
// ValueKey:
// ✓ Items with unique IDs
// ✓ Database IDs
// ✓ Email addresses
// ✓ Usernames

// ObjectKey:
// ✓ Complex objects
// ✓ Custom classes
// ✓ Objects with equality

// UniqueKey:
// ✓ Dynamic widgets
// ✓ Widgets without natural identity
// ✓ Animations

// GlobalKey:
// ✓ Form validation
// ✓ Accessing widget state
// ✓ Managing focus
// ✓ Testing

// PageStorageKey:
// ✓ List scroll positions
// ✓ Grid scroll positions
// ✓ Tab views

What's happening here? - ValueKey for IDs - ObjectKey for objects - UniqueKey for dynamic content - GlobalKey for global access - PageStorageKey for scroll


Keys in Lists

Keys are essential for efficient list updates.

// List with keys
class KeyedList extends StatefulWidget {
  const KeyedList({super.key});

  @override
  State<KeyedList> createState() => _KeyedListState();
}

class _KeyedListState extends State<KeyedList> {
  List<String> _items = ['A', 'B', 'C', 'D'];

  void _addItem() {
    setState(() {
      _items.insert(2, 'X');
    });
  }

  void _removeItem() {
    setState(() {
      if (_items.isNotEmpty) {
        _items.removeAt(1);
      }
    });
  }

  void _reorderItem() {
    setState(() {
      if (_items.length > 2) {
        final temp = _items[1];
        _items[1] = _items[2];
        _items[2] = temp;
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Row(
          children: [
            ElevatedButton(
              onPressed: _addItem,
              child: const Text('Add'),
            ),
            ElevatedButton(
              onPressed: _removeItem,
              child: const Text('Remove'),
            ),
            ElevatedButton(
              onPressed: _reorderItem,
              child: const Text('Reorder'),
            ),
          ],
        ),
        // With keys - efficient updates
        ..._items.map((item) {
          return Container(
            key: ValueKey(item), // Unique key
            color: Colors.blue,
            padding: const EdgeInsets.all(8),
            margin: const EdgeInsets.all(4),
            child: Text(item),
          );
        }).toList(),

        const Divider(),

        // Without keys - inefficient updates
        ..._items.map((item) {
          return Container(
            // No key
            color: Colors.green,
            padding: const EdgeInsets.all(8),
            margin: const EdgeInsets.all(4),
            child: Text(item),
          );
        }).toList(),
      ],
    );
  }
}

// ListView.builder with keys
class ListBuilderWithKeys extends StatelessWidget {
  const ListBuilderWithKeys({
    super.key,
    required this.items,
  });

  final List<String> items;

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: items.length,
      itemBuilder: (context, index) {
        return ListTile(
          // Key based on unique identifier
          key: ValueKey(items[index]),
          title: Text(items[index]),
        );
      },
    );
  }
}

What's happening here? - Keys enable efficient list updates - Without keys, all widgets rebuild - With keys, only changed widgets rebuild - Keys preserve state in lists - Use ValueKey for list items


GlobalKey in Detail

GlobalKey provides global access to widgets.

// GlobalKey usage
class GlobalKeyDetailed extends StatefulWidget {
  const GlobalKeyDetailed({super.key});

  @override
  State<GlobalKeyDetailed> createState() => _GlobalKeyDetailedState();
}

class _GlobalKeyDetailedState extends State<GlobalKeyDetailed> {
  // 1. Accessing state
  final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
  final GlobalKey<FormState> formKey = GlobalKey<FormState>();

  // 2. Accessing any widget
  final GlobalKey<State<StatefulWidget>> widgetKey = GlobalKey();

  // 3. Accessing specific widget type
  final GlobalKey<_CustomWidgetState> customKey = GlobalKey();

  void _useGlobalKeys() {
    // Access scaffold methods
    scaffoldKey.currentState?.showSnackBar(
      const SnackBar(content: Text('Hello')),
    );

    // Access form methods
    if (formKey.currentState?.validate() ?? false) {
      formKey.currentState?.save();
    }

    // Access widget state
    final widgetState = customKey.currentState;
    widgetState?.doSomething();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      key: scaffoldKey,
      body: Form(
        key: formKey,
        child: Column(
          children: [
            const Text('GlobalKey Example'),

            // Custom widget with GlobalKey
            CustomWidget(
              key: customKey,
            ),

            ElevatedButton(
              onPressed: _useGlobalKeys,
              child: const Text('Use GlobalKeys'),
            ),
          ],
        ),
      ),
    );
  }
}

class CustomWidget extends StatefulWidget {
  const CustomWidget({super.key});

  @override
  State<CustomWidget> createState() => _CustomWidgetState();
}

class _CustomWidgetState extends State<CustomWidget> {
  String _text = 'Initial';

  void doSomething() {
    setState(() {
      _text = 'Updated!';
    });
  }

  @override
  Widget build(BuildContext context) {
    return Text(_text);
  }
}

// GlobalKey rules:
// 1. GlobalKey must be unique
// 2. GlobalKey should not be rebuilt
// 3. GlobalKey persists across rebuilds
// 4. GlobalKey can find widget anywhere
// 5. GlobalKey works with StatefulWidget only

What's happening here? - GlobalKey accesses scaffold - GlobalKey validates forms - GlobalKey accesses custom state - GlobalKey enables global access - GlobalKey persists across rebuilds


Best Practices

Use Keys Only When Needed

// Good - Keys only when necessary
@override
Widget build(BuildContext context) {
  return ListView.builder(
    itemCount: items.length,
    itemBuilder: (context, index) {
      return ListTile(
        // Use key for dynamic lists
        key: ValueKey(items[index].id),
        title: Text(items[index].name),
      );
    },
  );
}

// Bad - Unnecessary keys
@override
Widget build(BuildContext context) {
  return Column(
    children: [
      // No key needed for static widgets
      const Text('Hello', key: UniqueKey()), // Unnecessary
      const Text('World', key: UniqueKey()), // Unnecessary
    ],
  );
}

Use Appropriate Key Types

// Good - ValueKey for unique IDs
@override
Widget build(BuildContext context) {
  return items.map((item) {
    return Text(
      item.name,
      key: ValueKey(item.id),
    );
  }).toList();
}

// Bad - UniqueKey for everything
@override
Widget build(BuildContext context) {
  return items.map((item) {
    return Text(
      item.name,
      key: UniqueKey(), // Causes unnecessary rebuilds
    );
  }).toList();
}

Store GlobalKeys Properly

// Good - Store in State
class _MyWidgetState extends State<MyWidget> {
  final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
  // ✓ Stored in State, persists
}

// Bad - Store in build
@override
Widget build(BuildContext context) {
  final GlobalKey<FormState> formKey = GlobalKey<FormState>();
  // ❌ Created every rebuild
}

Common Mistakes

Not Using Keys in Lists

Wrong:

// No keys
ListView.builder(
  itemCount: items.length,
  itemBuilder: (context, index) {
    return ListTile(
      title: Text(items[index]),
    );
  },
)

Correct:

// With keys
ListView.builder(
  itemCount: items.length,
  itemBuilder: (context, index) {
    return ListTile(
      key: ValueKey(items[index]),
      title: Text(items[index]),
    );
  },
)

Using Same Key Multiple Times

Wrong:

// Duplicate keys
Column(
  children: [
    Text('A', key: const ValueKey('same')),
    Text('B', key: const ValueKey('same')), // Duplicate key
  ],
)

Correct:

// Unique keys
Column(
  children: [
    Text('A', key: const ValueKey('a')),
    Text('B', key: const ValueKey('b')),
  ],
)


Summary

Keys identify widgets and help Flutter efficiently update the UI. Use keys for dynamic lists, reordering, and preserving state. Choose the appropriate key type (ValueKey, ObjectKey, UniqueKey, GlobalKey, PageStorageKey) based on your use case.


Next Steps


Did You Know?

  • Keys are optional for static widgets
  • Keys improve performance in lists
  • GlobalKey accesses widgets globally
  • PageStorageKey preserves scroll position
  • UniqueKey is always unique
  • ValueKey uses value equality
  • ObjectKey uses object identity
  • Keys should be unique in their scope