12 July 2021
Categories: Software Development
Posted in: Flutter, Dart, Android, iOS, Web, Widget
Goals
How does Flutter handle a widget's state?
In a typical application you will be creating Widgets that internally are made up, or composed, of other widgets (Widget Composition). In this scenario, it is often useful to pass objects from parent WidgetA to child WidgetB and ensure that WidgetB is rebuilt at the proper time by calling setState on WidgetA.
However, when we write our code we often see that the child widget doesn't rebuild as expected when calling setState on the parent, and we wonder what's wrong.
Let's see an example of this unexpected behaviour. In this example, WidgetA has a list of items and a button to add new items. When the button is pressed, WidgetA will pass a message to WidgetB to be dispayed on screen then call setState to rebuild the widget tree:
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: WidgetA(),
),
),
);
}
}
class WidgetA extends StatefulWidget {
@override
createState() => _WidgetAState();
}
class _WidgetAState extends State<WidgetA> {
List<String> items = [
"Item 1",
"Item 2",
];
@override
Widget build(BuildContext context) {
return Column(
children: [
ElevatedButton(
child: Text("Add Item"),
onPressed: () => setState(() {
items.add("Item ${items.length + 1}");
}),
),
WidgetB(
message: "There are ${items.length} items in the list.",
),
],
);
}
}
class WidgetB extends StatefulWidget {
WidgetB({ Key? key, required this.message }) : super(key: key);
final String message;
@override
createState() => _WidgetBState();
}
class _WidgetBState extends State<WidgetB> {
late String message;
@override
initState() {
super.initState();
message = widget.message;
}
@override
Widget build(BuildContext context) {
return Text(message);
}
}
You can test this code in DartPad (dartpad.dartlang.org)
In the onPressed event of the "Add Item" button, we add a new item then call setState in order to rebuild WidgetA and all it's inner widgets, including WidgetB:
onPressed: () => setState(() {
items.add("Item ${items.length + 1}");
}),
We run the application, tap the "Add Item" button only to discover that WidgetB has not been redrawn. If WidgetB was a stateless widget then this code would have worked, but WidgetB is a stateful widget so it will be rebuilt according to Flutter's state handling rules.
State handling in Flutter
Unless a stateful widget's position in the widgets tree has changed, or the type of the widget has changed, Flutter will preserve the widget's state and it will not force a rebuild of the stateful widget.
In our example, WidgetB's state hasn't changed at all from WidgetA's point of view so WidgetB will not be rebuilt. This is because the state change in B (the new message being updated in _WidgetBState.initState) is not visible from WidgetA.
Is there a way we can tell Flutter that the widget's state has indeed changed and the widget must be rebuilt? The solution is to pass a new Key to WidgetB every time we need it to be rebuilt: WidgetA will see that WidgetB has changed and will rebuild it when setState is called.
In other words, whenever a stateful widget's Key property changes, calling setState on its parent will force a rebuild of the stateful widget.
Widget Key
We can use any implementation of LocalKey to force our WidgetB to rebuild. The available implementations of LocalKey are: UniqueKey, ObjectKey, ValueKey.
Note: technically speaking we could also use an instance of Key and pass a different string every time we need to rebuild the widget, but implementations of LocalKey are much more versatile.
Here are the available widget keys:
Here's Dart code that demonstates how Flutter compares widget keys:
import 'package:flutter/material.dart';
void main() {
/// Key
print("Key");
print(Key("A") == Key("A"));
print(Key("A") != Key("B"));
/// GlobalKey
print("\nGlobalKey");
print(GlobalKey() != GlobalKey());
/// UniqueKey
print("\nUniqueKey");
print(UniqueKey() != UniqueKey());
/// ValueKey<T>
print("\nValueKey<T>");
print(ValueKey<String>("A") == ValueKey<String>("A"));
print(ValueKey<String>("A") != ValueKey<String>("B"));
/// ObjectKey
print("\nObjectKey");
print(ObjectKey("A") == ObjectKey("A"));
print(ObjectKey("A") != ObjectKey("B"));
String id = "id1";
print(ObjectKey(id) == ObjectKey(id));
var obj1 = TestObject("id1");
var obj2 = obj1;
print(ObjectKey(obj1) == ObjectKey(obj2));
print(obj1 == obj2);
}
class TestObject {
TestObject(this.id);
final String id;
}
You can test this code in DartPad (dartpad.dartlang.org)
Using UniqueKey to rebuild the widget
Back to our example, we asked ourselved: how do we tell Flutter that WidgetB has changed and must be rebuilt when WidgetA calls setState? We now know that we can accomplish that by adding a UniqueKey to WidgetB.
Here's the edited WidgetA state class that will accomplish that:
class _WidgetAState extends State<WidgetA> {
List<String> items = [
"Item 1",
"Item 2",
];
@override
Widget build(BuildContext context) {
return Column(
children: [
ElevatedButton(
child: Text("Add Item"),
onPressed: () => setState(() {
items.add("Item ${items.length + 1}");
}),
),
WidgetB(
key: UniqueKey(), /// Here's our UniqueKey
message: "There are ${items.length} items in the list.",
),
],
);
}
}
As expected, this code will rebuild WidgetB every time that WidgetA calls setState.
This solution will work but it is not really optimal, performance wise, because we want to rebuild WidgetB only when it's state has really changed, that is when WidgetA updates the message shown by WidgetB, not simply on every setState call on WidgetA.
In order to rebuild our WidgetB only when really needed, we can use a ValueKey or an ObjectKey.
Using ValueKey to rebuild the widget
As we have already learned, a ValueKey is a key that uses a value of a particular type to identify itself. To demonstrate this, in the next code example the ValueKey will change every time the number of items in the list has changed because we are passing the length of the item array to it.
Here's the edited WidgetA state class:
class _WidgetAState extends State<WidgetA> {
List<String> items = [
"Item 1",
"Item 2",
];
@override
Widget build(BuildContext context) {
return Column(
children: [
ElevatedButton(
child: Text("Add Item"),
onPressed: () => setState(() {
items.add("Item ${items.length + 1}");
}),
),
WidgetB(
key: ValueKey<int>(items.length), /// Here's our ValueKey
message: "There are ${items.length} items in the list.",
),
],
);
}
}
As we can see, WidgetB will be rebuilt only if the number of items in the list has changed. Any other call to setState from WidgetA will not cause WidgetB to be rebuilt. This is important because we want to rebuild our widgets only when strictly necessary (rebuilding widgets is a costly operation that impacts App performance negatively).
Using ObjectKey to rebuild the widget
We can achieve the same result also by using an ObjectKey. For completeness, let's see how that is done.
We have learned that an ObjectKey is a key that takes its identity from the object instance used as its value. In order to leverage this particular key, all we need to do is pass a different object every time we want to rebuild WidgetB.
In the following example, we will pass the last inserted item to the ObjectKey. This way WidgetA will rebuild WidgetB every time we have added a new item to the list, as follows:
class _WidgetAState extends State<WidgetA> {
List<String> items = [
"Item 1",
"Item 2",
];
@override
Widget build(BuildContext context) {
return Column(
children: [
ElevatedButton(
child: Text("Add Item"),
onPressed: () => setState(() {
items.add("Item ${items.length + 1}");
}),
),
WidgetB(
key: ObjectKey(items.last), /// Here's our ObjectKey
message: "There are ${items.length} items in the list.",
),
],
);
}
}
We can now see that WidgetB is rebuilt every time the last item in the list has changed, following a tap on the "Add item" button.
Conclusions
In this article we have learned how to use a stateful widget's Key to force it to be rebuilt when needed.
Remember that you can test this article's code in DartPad (dartpad.dartlang.org)
Happy Coding ;)