Using GlobalKey and LocalKey for Specific Scenarios in Flutter

In Flutter, keys are essential for maintaining the state of StatefulWidgets when their position in the widget tree changes. Two primary types of keys are GlobalKey and LocalKey (including ValueKey, ObjectKey, and UniqueKey). Understanding when to use each key is crucial for developing robust Flutter applications.

Understanding Keys in Flutter

Keys are identifiers for Widgets, Elements, and States in the Flutter framework. They help Flutter identify which part of the widget tree to preserve when the tree is rebuilt. Keys are important when dealing with lists of widgets that can be reordered or dynamically updated.

LocalKey vs. GlobalKey: Key Differences

  • LocalKey: Scoped within the same parent. Useful for reordering items in a list, as it ensures the state persists with the correct widget. Examples include ValueKey, ObjectKey, and UniqueKey.
  • GlobalKey: Unique across the entire app. Used to access a widget’s State from anywhere in the widget tree. Useful for interacting with a specific widget directly.

LocalKey (ValueKey, ObjectKey, UniqueKey)

LocalKeys are primarily used to maintain the identity of widgets relative to their parent. Let’s explore the different types of LocalKey:

ValueKey

ValueKey is useful when you want to associate a widget with a specific, meaningful value. The key is based on the value provided, allowing Flutter to track widgets based on their data rather than their position in the tree.

Example: Using ValueKey in a Reorderable List

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: ReorderableListExample(),
    );
  }
}

class ReorderableListExample extends StatefulWidget {
  @override
  _ReorderableListExampleState createState() => _ReorderableListExampleState();
}

class _ReorderableListExampleState extends State {
  List items = ['Item 1', 'Item 2', 'Item 3', 'Item 4'];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Reorderable List')),
      body: ReorderableListView(
        children: items.map((item) => Card(
          key: ValueKey(item), // Use ValueKey with the item value
          elevation: 2,
          margin: EdgeInsets.all(8),
          child: ListTile(
            title: Text(item),
          ),
        )).toList(),
        onReorder: (oldIndex, newIndex) {
          setState(() {
            if (oldIndex < newIndex) {
              newIndex -= 1;
            }
            final String item = items.removeAt(oldIndex);
            items.insert(newIndex, item);
          });
        },
      ),
    );
  }
}

In this example:

  • Each Card in the ReorderableListView is given a ValueKey based on the item’s value.
  • When the list is reordered, Flutter uses the ValueKey to ensure the correct State is associated with the correct item, even after the widget tree is rebuilt.

ObjectKey

ObjectKey is useful when you want to associate a widget with a specific object instance. The key is based on the object provided, and it’s primarily used when you want to ensure that the same widget remains associated with the same object, even if the widget’s data changes.

Example: Using ObjectKey with Mutable Objects

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: ObjectKeyExample(),
    );
  }
}

class MyObject {
  String name;
  MyObject(this.name);
}

class ObjectKeyExample extends StatefulWidget {
  @override
  _ObjectKeyExampleState createState() => _ObjectKeyExampleState();
}

class _ObjectKeyExampleState extends State {
  List objects = [MyObject('Object 1'), MyObject('Object 2')];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('ObjectKey Example')),
      body: Column(
        children: [
          ...objects.map((obj) => MyWidget(key: ObjectKey(obj), object: obj)).toList(),
          ElevatedButton(
            onPressed: () {
              setState(() {
                objects[0].name = 'Updated Object 1';
              });
            },
            child: Text('Update Object 1'),
          ),
        ],
      ),
    );
  }
}

class MyWidget extends StatefulWidget {
  final MyObject object;
  MyWidget({Key? key, required this.object}) : super(key: key);

  @override
  _MyWidgetState createState() => _MyWidgetState();
}

class _MyWidgetState extends State {
  @override
  Widget build(BuildContext context) {
    return Card(
      elevation: 2,
      margin: EdgeInsets.all(8),
      child: ListTile(
        title: Text(widget.object.name),
      ),
    );
  }
}

In this example:

  • Each MyWidget is given an ObjectKey based on a unique instance of MyObject.
  • Even if the properties of MyObject change, Flutter retains the widget’s state because the ObjectKey remains associated with the same object instance.

UniqueKey

UniqueKey is used when you need to force a widget to be recreated. It generates a new unique key each time, which ensures that Flutter treats the widget as a completely new instance.

Example: Forcing Rebuild with UniqueKey

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: UniqueKeyExample(),
    );
  }
}

class UniqueKeyExample extends StatefulWidget {
  @override
  _UniqueKeyExampleState createState() => _UniqueKeyExampleState();
}

class _UniqueKeyExampleState extends State {
  bool rebuild = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('UniqueKey Example')),
      body: Column(
        children: [
          MyStatefulWidget(key: rebuild ? UniqueKey() : ValueKey('stable')),
          ElevatedButton(
            onPressed: () {
              setState(() {
                rebuild = !rebuild;
              });
            },
            child: Text('Toggle Rebuild'),
          ),
        ],
      ),
    );
  }
}

class MyStatefulWidget extends StatefulWidget {
  MyStatefulWidget({Key? key}) : super(key: key);

  @override
  _MyStatefulWidgetState createState() => _MyStatefulWidgetState();
}

class _MyStatefulWidgetState extends State {
  int counter = 0;

  @override
  void initState() {
    super.initState();
    print('Widget created');
  }

  @override
  void dispose() {
    print('Widget disposed');
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Card(
      elevation: 2,
      margin: EdgeInsets.all(8),
      child: ListTile(
        title: Text('Counter: $counter'),
        trailing: ElevatedButton(
          onPressed: () {
            setState(() {
              counter++;
            });
          },
          child: Text('Increment'),
        ),
      ),
    );
  }
}

In this example:

  • The MyStatefulWidget is given a UniqueKey or a ValueKey based on the rebuild flag.
  • When rebuild is true, a new UniqueKey is generated, causing Flutter to dispose of the old widget and create a new one, resetting its state.
  • When rebuild is false, the ValueKey remains the same, and the widget’s state is preserved.

GlobalKey

GlobalKey provides a way to access a widget’s State from anywhere in the application. It is generally used sparingly because it can make code harder to maintain. GlobalKey is particularly useful when you need to perform actions on a widget from a distant part of the widget tree.

Example: Using GlobalKey to Access a Widget’s State


import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: GlobalKeyExample(),
    );
  }
}

class GlobalKeyExample extends StatefulWidget {
  @override
  _GlobalKeyExampleState createState() => _GlobalKeyExampleState();
}

class _GlobalKeyExampleState extends State {
  final GlobalKey myWidgetKey = GlobalKey();

  void incrementCounter() {
    myWidgetKey.currentState?.incrementCounter();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('GlobalKey Example')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            MyWidget(key: myWidgetKey),
            SizedBox(height: 20),
            ElevatedButton(
              onPressed: incrementCounter,
              child: Text('Increment Counter in MyWidget'),
            ),
          ],
        ),
      ),
    );
  }
}

class MyWidget extends StatefulWidget {
  MyWidget({Key? key}) : super(key: key);

  @override
  MyWidgetState createState() => MyWidgetState();
}

class MyWidgetState extends State {
  int counter = 0;

  void incrementCounter() {
    setState(() {
      counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Card(
      elevation: 2,
      margin: EdgeInsets.all(8),
      child: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Text('Counter: $counter'),
      ),
    );
  }
}

In this example:

  • A GlobalKey is created (myWidgetKey) and associated with MyWidget.
  • The incrementCounter method in _GlobalKeyExampleState uses the GlobalKey to access the MyWidgetState and call its incrementCounter method.
  • This allows _GlobalKeyExampleState to directly manipulate the state of MyWidget.

Best Practices for Using Keys

  • Use LocalKey when reordering or dynamically updating lists. It helps Flutter maintain the correct widget state.
  • Use ValueKey when you need to associate widgets with meaningful data.
  • Use ObjectKey when you want to maintain the association between a widget and a specific object instance, even if the object’s properties change.
  • Use UniqueKey when you want to force a widget to be recreated, resetting its state.
  • Use GlobalKey sparingly. Overuse can lead to tightly coupled and harder-to-maintain code.

Conclusion

Understanding the differences between LocalKey and GlobalKey and when to use them is essential for writing robust Flutter applications. LocalKeys (including ValueKey, ObjectKey, and UniqueKey) are suitable for managing widgets within a local context, while GlobalKeys are used to access widget state from anywhere in the app. By correctly applying these concepts, you can ensure that your Flutter applications handle state management efficiently and predictably.