How to force a widget to redraw in Flutter

Categories: Software Development

Posted in: Flutter, Dart, Android, iOS, Web, Widget

Goals

  • How does Flutter handle state?
  • Types of widget keys
  • Forcing a widget rebuild using a UniqueKey
  • Forcing a widget rebuild using a ValueKey
  • Forcing a widget rebuild using an ObjectKey

 

 

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)Open link in a new tab


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:

  • Key(String) is a key identified by the given String. For example, Key("Hello") is different from Key("Goodbye")
  • UniqueKey() is a key guaranteed to be unique for each instance of it (an instance of UniqueKey is only equal to itself)
  • ValueKey<T>(T) is a key that uses a value of a particular type to identify itself. For example, ValueKey<int>(1) is different from ValueKey<int>(2)
  • ObjectKey(Object) is a key that takes its identity from the object instance used as its value. For example, ObjectKey(5) is different from ObjectKey(7)
  • GlobalKey() is a key that is unique across the entire app. This kind of key does not apply to this article's topic so I will not explain it further.

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)Open link in a new tab

 

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)Open link in a new tab

 

Happy Coding ;)

 

 

Author

Alex Domenici