WorkServicesArticlesCareers
Flutter Logo

#flutter

#dart

#tips

Save time! Optimizing Rebuilds With Flutter

Alberto Miola   Engineering team

,

Welcome to a new blog post about the Flutter framework. Today, we are focusing on how you can save widget rebuilds to make your apps more performant.

Quick Dart recap

Before diving into the details, we want to briefly review how the Dart language treats constant objects. For example, consider this extremely simple piece of code:

class Example {
  const Example();
}

void main() {
  print(Example() == Example()); // false
  print(const Example() == const Example()); // true
}

Since we haven’t overridden operator== and hashCode, the first print statement returns false because it’s comparing two different object references.

A constant constructor instead creates at compile-time a single, canonical instance which points, under the hood, to the same object. As such, the const Example() == const Example() is actually comparing the same instance! You can also test this by yourself:

class Example {
  const Example();
}

void main() {
  const a = Example();
  const b = Example();
  
  print(identical(a, b)); // true
}

The identical method, part of the code Dart SDK, checks whether two references actually point to the same object. Even if we created thousands of const Example() object, the reference would always point to the same object.

Constant constructors are important in Flutter

Whenever we rebuild a widget, along with its children, Flutter tries to minimize the amount of work where possible. When we call setState for example, the Element associated to our widget is marked as dirty and added to a global list of widgets to be rebuilt on the next frame.

When it’s time to rebuild a widget, Flutter calls the Element.updateChild method. This is the skeleton of the method, without all the assertions and profiling utility calls:

// 'child' is the old widget that needs to be removed.
// 'newWidget' is the new widget to be inserted in the tree
Element? updateChild(Element? child, Widget? newWidget, Object? newSlot) {
  if (newWidget == null) { // 1
    return null;
  }

  final Element newChild;
  if (child != null) {
    if (child.widget == newWidget) { // 2
      newChild = child;
    } else if (Widget.canUpdate(child.widget, newWidget)) { // 3
      child.update(newWidget);
      newChild = child;
    } else {
      deactivateChild(child);
      newChild = inflateWidget(newWidget, newSlot); // 4
    }
  }

  return newChild;
}

The updateChild method is at the core of the widget system because it handles the entire lifetime of the widget tree. Each number we’ve commented out in the code is associated to a point in this bullet list, to help you keep track (in order) to what happens:

  1. If newWidget is null, then it means that the widget to be rebuilt (child) has been removed and so it also has to disappear from the UI. To do so, we just return null so that the widget will be removed from the widget tree.
  2. This is the key point. If the widget to be rebuilt is created with a const constructor, then this child.widget == newWidget call will always return true. As such, the framework just passes a reference without rebuilding or touching anything. Awesome!
  3. If the widget to be rebuilt isn’t constant, then we can try using the canUpdate static method. It checks whether the old and the new widget have the same runtime type and key. If this is true, then Flutter calls child.update (which is very cheap) to update the internal reference of the Element. Still no rebuilds here!
  4. At this point, Flutter cannot reuse anything else and thus calls the inflateWidget method that actually triggers a rebuild.

From point number 2 we can understand that constant constructors can save entire subtrees from being rebuilt! Whenever you can, always try to split your widgets into smaller ones and use const constructors as much as possible.

Don’t use functions to return widgets

You may be tempted to wrap reusable widgets with a function rather than creating a StatelessWidget because it requires less code:

Code Sample side by side

The function allows you to write less code but in Dart you cannot use a constant constructor in front of a function! As such, cannot do this...

@override
Widget build(BuildContext context) {
  return const buildContainer(); // Compiler error!!!
}

... but can do this, which is much better:

@override
Widget build(BuildContext context) {
  return const Example();
}

Since we have seen that using constant constructors can be a huge gain, especially on larger subtrees, so you should always prefer widgets over functions.

Manually caching widgets

Sometimes, you may wish to create a constant widget but you cannot because it has some external dependencies that forbids the const usage. For example, look at this Container:

class Example extends StatelessWidget {
  final Color color;
  const Example({
    super.key,
    required this.color,
  });

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        const Text('Here we have the list:'),
        const SizedBox(height: 20),
        Container(
          height: 30,
          width: 30,
          decoration: BoxDecoration(
            color: Colors.blue,
            borderRadius: BorderRadius.all(Radius.circular(10)),
          ),
          child: SomeOtherWidget(
            color: color,
          ),
        ),
      ],
    );
  }
}

Both Container and its child depend on color, but it may not always change. It may not change at all but we still cannot use const. In this case, we can manually cache the widget:

class Example extends StatefulWidget {
  final Color color;
  const Example({
    super.key,
    required this.color,
  });

  @override
  State<Example> createState() => _ExampleState();
}

class _ExampleState extends State<Example> {
  late var child = _ExampleChild(widget.color);

  @override
  void didUpdateWidget(covariant Example oldWidget) {
    super.didUpdateWidget(oldWidget);

    if (widget.color != oldWidget.color) {
      child = _ExampleChild(widget.color);
    }
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        const Text('Here we have the list:'),
        const SizedBox(height: 20),
        child,
      ],
    );
  }
}

class _ExampleChild extends StatelessWidget {
  final Color color;
  const _ExampleChild(this.color);

  @override
  Widget build(BuildContext context) {
    return Container(
      height: 30,
      width: 30,
      decoration: BoxDecoration(
        color: Colors.blue,
        borderRadius: BorderRadius.all(Radius.circular(10)),
      ),
      child: SomeOtherWidget(
        color: color,
      ),
    );
  }
}

There is some more code to write but the result is the same as using a const constructor! Since the widget is now located in the state...

class _ExampleState extends State<Example> {
  late var child = _ExampleChild(widget.color);

... whenever Example is rebuilt, the build method always passes the same reference to the widget. As such, the child.widget == newWidget evaluation inside the Element.update method returns true and _ExampleWidget is not rebuilt.

Wow, you made it until the end

We hope that this article has been helpful to you. If you have any other questions or comments, mention us on Twitter and we'll respond.

More Articles

Article Cover

#experience

#culture

In Sync: Why True Partnerships are the Fuel for Modernizing Technology

 
Article Cover

#experience

#culture

#flutter

From Ideation to Delivery: Pushing the Boundaries of Expectations and Technology

 
Logo

Whether it’s our services that speak to you or joining our team seems like a dream come true, get in touch, or follow along on all our digital adventures.


©2022 Superformula. All rights reserved.

👋  We use cookies to improve the user-experience of this website.

OK