Understanding Keys in Flutter: GlobalKey, LocalKey, ValueKey and UniqueKey

How to Manage Widget Uniqueness and State in Flutter

Tomic Riedel
5 min readMay 6, 2024
Icon by Freepik — Flaticon

In Flutter, keys play a crucial role in managing the identity of widgets within your app. They help Flutter differentiate between widgets, maintain states, and optimize the rendering process.

Whether it’s about uniquely identifying a widget throughout the app or just within a specific part of the UI, different types of keys serve different purposes. This article explores the key differences and uses of GlobalKey, LocalKey, ValueKey, and UniqueKey, helping you understand how and when to use them effectively in your Flutter projects.

GlobalKey

GlobalKey is a kind of key that allows you to uniquely identify a widget across the widget tree. Unlike LocalKeys, which are used for ensuring uniqueness within a specific level of the tree (More on that further down below), GlobalKey provides global access to a widget, making it useful in scenarios where you need to reference a widget outside its immediate parent.

Global keys are especial when you want to access the state of a stateful widget from outside the widget tree. Or you want to maintain a state of a widget even when it moves around in the widget tree. It is also useful when you want to navigate to a particular widget. This is typically done for testing or animations.

Let’s take a look at the following example to see where global keys are useful:

import 'package:flutter/material.dart';

void main() {
runApp(MyApp());
}

class MyApp extends StatelessWidget {
// Define a GlobalKey for accessing the state of CounterWidget
final GlobalKey<CounterWidgetState> _counterKey = GlobalKey<CounterWidgetState>();

@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('GlobalKey Example')),
body: Center(
child: CounterWidget(key: _counterKey),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
// Access the CounterWidgetState to increment the counter
_counterKey.currentState?.incrementCounter();
},
child: Icon(Icons.add),
),
),
);
}
}

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

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

class CounterWidgetState extends State<CounterWidget> {
int _counter = 0;

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

@override
Widget build(BuildContext context) {
return Text(
'Counter: $_counter',
style: TextStyle(fontSize: 24),
);
}
}

How it Works:

  1. A GlobalKey instance is created (_counterKey), which is passed to the CounterWidget.
  2. In the CounterWidget state class (CounterWidgetState), a method incrementCounter is defined to increment the counter.
  3. The FloatingActionButton uses the GlobalKey to access the incrementCounter method of the CounterWidgetState from outside the widget tree.

This example shows how GlobalKey allows external control over widgets, which can be especially useful in scenarios like testing, widget manipulation, or maintaining widget states during structural changes.

LocalKey

LocalKey is a class that serves as the base class for keys that are used to uniquely identify widgets within the same subtree. Unlike GlobalKey, which uniquely identifies a widget across the entire app, LocalKey ensures uniqueness only within the subtree where it's used.

LocalKey itself is not directly instantiated; instead, it has two concrete subclasses that are used in practice:

1. ValueKey<T>:

ValueKey is the most commonly used key that extends LocalKey. It’s designed to identify widgets based on a specific value of type T, ensuring that widgets with the same ValueKey are considered identical by the framework. This helps maintain the state of a widget even if it gets rearranged in the widget tree.

You should use a ValueKey when you:

  1. Have lists with Identifiable Items: Ideal for widgets in lists where items have a unique identifier (e.g., a string, integer, or other simple types).
  2. Want to Maintain State: When you want to ensure a widget keeps its state based on its value.
  3. Want to Do Conditional Rendering: Useful for distinguishing between different conditions of the same type of widget, depending on its associated value.

An example of a ValueKey is a list where each widget is identified by a string value:

import 'package:flutter/material.dart';

void main() {
runApp(MyApp());
}

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('ValueKey Example')),
body: FruitList(),
),
);
}
}

class FruitList extends StatefulWidget {
@override
_FruitListState createState() => _FruitListState();
}

class _FruitListState extends State<FruitList> {
final List<String> _fruits = ['Apple', 'Banana', 'Cherry', 'Date', 'Elderberry'];

void _shuffleFruits() {
setState(() {
_fruits.shuffle();
});
}

@override
Widget build(BuildContext context) {
return Column(
children: [
ElevatedButton(
onPressed: _shuffleFruits,
child: Text('Shuffle Fruits'),
),
Expanded(
child: ListView.builder(
itemCount: _fruits.length,
itemBuilder: (context, index) {
return ListTile(
key: ValueKey(_fruits[index]), // ValueKey to uniquely identify each ListTile
title: Text(_fruits[index]),
);
},
),
),
],
);
}
}

Each ListTile is assigned a ValueKey with the string representing the fruit's name as its value. This ensures that each item retains its identity based on the fruit name.

Even when the fruit list is shuffled, the ValueKey helps ensure that the widgets maintain their state and identity.

2. UniqueKey:

UniqueKey generates a new key with a unique identity each time it is created. It generates a new key instance with a unique identity every time it’s created. This is used to ensure that a widget is uniquely identified within its parent subtree. The primary purpose of UniqueKey is to force the framework to treat the widget as a new entity, which is especially useful in lists and other dynamic content where you want to ensure that a widget is not reused or confused with others.

You should use UniqueKey with:

  1. Dynamic Content: When you have dynamic content that is regenerated frequently, such as in lists or other collections.
  2. Animations: When you want to force a widget to be rebuilt to create a fresh instance for animations or transitions.
  3. Testing: To verify the widget tree’s behavior under conditions where items change frequently.

In the following example, we can see how UniqueKey forces Framework to treat a widget as new:

import 'package:flutter/material.dart';

void main() {
runApp(MyApp());
}

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('UniqueKey Example')),
body: NumberList(),
),
);
}
}

class NumberList extends StatefulWidget {
@override
_NumberListState createState() => _NumberListState();
}

class _NumberListState extends State<NumberList> {
final List<int> _numbers = [1, 2, 3, 4, 5];

void _shuffleNumbers() {
setState(() {
_numbers.shuffle();
});
}

@override
Widget build(BuildContext context) {
return Column(
children: [
ElevatedButton(
onPressed: _shuffleNumbers,
child: Text('Shuffle Numbers'),
),
Expanded(
child: ListView.builder(
itemCount: _numbers.length,
itemBuilder: (context, index) {
return ListTile(
key: UniqueKey(), // Each ListTile is uniquely identified
title: Text('Number: ${_numbers[index]}'),
);
},
),
),
],
);
}
}

Each ListTile in the ListView is given a UniqueKey, which ensures that Flutter recognizes each ListTile as a unique entity.

When the numbers are shuffled, the UniqueKey ensures that the previous widgets are discarded, and new ones are created, even though the number of widgets in the list remains the same.

Quick Recap

Let’s do a recap and see what each key does:

1. GlobalKey:

  • Scope: Global, identifies a widget across the entire widget tree.
  • Usage: Access and manipulate widget state from outside its parent subtree.

2. LocalKey:

  • Scope: Local, identifies a widget within its parent subtree.
  • Usage: Base class, not used directly; provides uniqueness within a subtree.

2.1. ValueKey:

  • Subclass of LocalKey
  • Scope: Local, identifies a widget within its parent subtree based on a specific value.
  • Usage: Typically used in lists or collections to uniquely identify items by their associated value.

2.2. UniqueKey:

  • Subclass of LocalKey
  • Scope: Local, identifies a widget uniquely each time it’s created.
  • Usage: Force the framework to treat the widget as new and unique in dynamic content or testing scenarios.

Finishing Up

That’s it for today, thank you so much for reading!

If you are interested in more Flutter content, consider subscribing to my recently started newsletter: https://tomicriedel.substack.com/

--

--

Tomic Riedel
Tomic Riedel

Written by Tomic Riedel

Sharing the process of building a portfolio of apps to make people more productive.