Question #81EasyFlutter BasicsImportant

What is keys in flutter and when to use key in flutter ?

#flutter

Answer

Keys in Flutter

Keys are identifiers for widgets, elements, and semantic nodes in Flutter. They help Flutter identify which widgets have changed, been added, or removed when the widget tree is rebuilt. Keys are essential for maintaining state and optimizing widget updates.

When to Use Keys

ScenarioNeed Key?Reason
Stateless widgets (no reordering)❌ NoFlutter can track by position
Stateful widgets (reordering)✅ YesNeed to preserve state
List items being reordered✅ YesMaintain identity during reorder
Swapping widget positions✅ YesDistinguish between widgets
Modifying collection (add/remove)✅ YesTrack individual items
Static widget tree❌ NoNo changes to track

Types of Keys

1. ValueKey

Uses a specific value as the key identifier. Best for simple, immutable values.

dart
// Example: Todo list with ValueKey
class TodoList extends StatelessWidget {
  final List<String> todos = ['Buy milk', 'Walk dog', 'Write code'];

  
  Widget build(BuildContext context) {
    return ListView(
      children: todos.map((todo) {
        return ListTile(
          key: ValueKey(todo), // Uses todo text as key
          title: Text(todo),
        );
      }).toList(),
    );
  }
}

2. ObjectKey

Uses an object's identity as the key. Best when you have complex objects with unique identities.

dart
class User {
  final int id;
  final String name;

  User(this.id, this.name);
}

class UserList extends StatelessWidget {
  final List<User> users = [
    User(1, 'Alice'),
    User(2, 'Bob'),
    User(3, 'Charlie'),
  ];

  
  Widget build(BuildContext context) {
    return ListView(
      children: users.map((user) {
        return ListTile(
          key: ObjectKey(user), // Uses user object as key
          title: Text(user.name),
        );
      }).toList(),
    );
  }
}

3. UniqueKey

Generates a unique key that never equals another key. Best when you need guaranteed uniqueness but don't have a natural identifier.

dart
class DynamicList extends StatefulWidget {
  
  _DynamicListState createState() => _DynamicListState();
}

class _DynamicListState extends State<DynamicList> {
  List<Widget> items = [];

  void addItem() {
    setState(() {
      items.add(
        Container(
          key: UniqueKey(), // Each widget gets unique key
          height: 50,
          color: Colors.blue,
        ),
      );
    });
  }

  
  Widget build(BuildContext context) {
    return Column(children: items);
  }
}

4. GlobalKey

Allows you to access the widget's state from anywhere in the app. Use sparingly as it can create tight coupling.

dart
class FormExample extends StatelessWidget {
  final GlobalKey<FormState> _formKey = GlobalKey<FormState>();

  
  Widget build(BuildContext context) {
    return Form(
      key: _formKey, // GlobalKey for form
      child: Column(
        children: [
          TextFormField(
            validator: (value) {
              if (value == null || value.isEmpty) {
                return 'Please enter text';
              }
              return null;
            },
          ),
          ElevatedButton(
            onPressed: () {
              // Access form state from anywhere using global key
              if (_formKey.currentState!.validate()) {
                print('Form is valid!');
              }
            },
            child: Text('Submit'),
          ),
        ],
      ),
    );
  }
}

5. PageStorageKey

Preserves scroll position and other page state when navigating away and back.

dart
class TabViewExample extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return DefaultTabController(
      length: 2,
      child: Scaffold(
        appBar: AppBar(
          bottom: TabBar(
            tabs: [Tab(text: 'Tab 1'), Tab(text: 'Tab 2')],
          ),
        ),
        body: TabBarView(
          children: [
            ListView.builder(
              key: PageStorageKey('tab1'), // Preserves scroll position
              itemCount: 100,
              itemBuilder: (context, index) => ListTile(title: Text('Item $index')),
            ),
            ListView.builder(
              key: PageStorageKey('tab2'), // Preserves scroll position
              itemCount: 100,
              itemBuilder: (context, index) => ListTile(title: Text('Item $index')),
            ),
          ],
        ),
      ),
    );
  }
}

Practical Example: Reorderable List with Keys

Without Keys (Broken)

dart
class CounterTile extends StatefulWidget {
  
  _CounterTileState createState() => _CounterTileState();
}

class _CounterTileState extends State<CounterTile> {
  int count = 0;

  
  Widget build(BuildContext context) {
    return ListTile(
      title: Text('Count: $count'),
      trailing: IconButton(
        icon: Icon(Icons.add),
        onPressed: () => setState(() => count++),
      ),
    );
  }
}

class BrokenList extends StatefulWidget {
  
  _BrokenListState createState() => _BrokenListState();
}

class _BrokenListState extends State<BrokenList> {
  List<CounterTile> items = [CounterTile(), CounterTile()];

  void swap() {
    setState(() {
      final temp = items[0];
      items[0] = items[1];
      items[1] = temp;
    });
  }

  
  Widget build(BuildContext context) {
    return Column(
      children: [
        ...items,
        ElevatedButton(
          onPressed: swap,
          child: Text('Swap'),
        ),
      ],
    );
  }
}
// Problem: Swapping doesn't preserve individual counter states

With Keys (Fixed)

dart
class FixedList extends StatefulWidget {
  
  _FixedListState createState() => _FixedListState();
}

class _FixedListState extends State<FixedList> {
  List<CounterTile> items = [
    CounterTile(key: UniqueKey()),
    CounterTile(key: UniqueKey()),
  ];

  void swap() {
    setState(() {
      final temp = items[0];
      items[0] = items[1];
      items[1] = temp;
    });
  }

  
  Widget build(BuildContext context) {
    return Column(
      children: [
        ...items,
        ElevatedButton(
          onPressed: swap,
          child: Text('Swap'),
        ),
      ],
    );
  }
}
// Solution: Keys preserve state during reordering

How Keys Work Internally

When Flutter rebuilds the widget tree:

  1. Without Keys: Matches widgets by type and position
  2. With Keys: Matches widgets by type and key
dart
// Example: Flutter's matching algorithm

// Initial build
Row(
  children: [
    Container(color: Colors.red),    // Position 0
    Container(color: Colors.blue),   // Position 1
  ],
)

// After rebuild (without keys)
Row(
  children: [
    Container(color: Colors.blue),   // Matches position 0 (WRONG!)
    Container(color: Colors.red),    // Matches position 1 (WRONG!)
  ],
)

// After rebuild (with keys)
Row(
  children: [
    Container(key: ValueKey('blue'), color: Colors.blue),  // Matches by key ✅
    Container(key: ValueKey('red'), color: Colors.red),    // Matches by key ✅
  ],
)

Key Selection Guide

Use CaseRecommended Key TypeExample
Simple values (String, int)
text
ValueKey
text
ValueKey('item-1')
Complex objects
text
ObjectKey
text
ObjectKey(user)
Always unique items
text
UniqueKey
text
UniqueKey()
Access state globally
text
GlobalKey
text
GlobalKey<FormState>()
Preserve scroll position
text
PageStorageKey
text
PageStorageKey('list')

Best Practices

  1. Use Keys Sparingly: Only when needed for correctness
  2. Choose the Right Key Type: Match the key type to your use case
  3. Avoid GlobalKey Unless Necessary: It creates tight coupling
  4. Keys Must Be Unique Among Siblings: Not globally unique (except UniqueKey)
  5. Keys Should Be Stable: Don't generate new keys on every build
dart
// ❌ Bad: Generating new key on every build
Widget build(BuildContext context) {
  return ListTile(
    key: UniqueKey(), // New key every rebuild!
    title: Text('Item'),
  );
}

// ✅ Good: Stable key
final itemKey = UniqueKey(); // Created once

Widget build(BuildContext context) {
  return ListTile(
    key: itemKey, // Same key across rebuilds
    title: Text('Item'),
  );
}

Common Pitfalls

dart
// ❌ Bad: Using index as key in reorderable list
ListView.builder(
  itemBuilder: (context, index) {
    return ListTile(
      key: ValueKey(index), // Index changes when items move!
      title: Text(items[index]),
    );
  },
);

// ✅ Good: Using item ID as key
ListView.builder(
  itemBuilder: (context, index) {
    return ListTile(
      key: ValueKey(items[index].id), // ID stays with item
      title: Text(items[index].name),
    );
  },
);

Debugging Keys

Enable key debugging to see how Flutter uses keys:

dart
void main() {
  debugPrintGlobalKeyedWidgetLifecycle = true;
  runApp(MyApp());
}

Important: Keys are Flutter's way of maintaining widget identity. Use them when widget order changes, but don't overuse them as they add overhead.

Documentation: Flutter Keys Documentation