What Happens When Build Runs In Flutter

What Happens When Build Runs In Flutter

I used to think everything on the screen gets redrawn by flutter pixel by pixel when setstate is called. That would actually result in performance issues if this was the case. In this article we are going to see what actually happens when build runs.

Flutter aims to provide 60 frames per second (fps) performance, or 120 fps performance on devices capable of 120Hz updates. For 60fps, frames need to render approximately every 16ms. The first time a screen is displayed flutter needs to figure out every information like color, size, position etc in order to paint it on the screen, and for subsequent build It simply takes the old information and repaints that on the screen which is fast and efficient. It would be inefficient if it had to re calculate the entire layout information every 16ms.

There are three layers that need to be covered to understand how build works

The Widget Tree

This is the code we write and control. So the flutter widgets we use and the once we create all make up the widget tree. Every of these widget has a build method which are called often to update the screen. The widget tree doesn't handle what's been drawn on the screen, think of it more as a detailed description / configuration of what should be drawn.

The Element Tree

For every widget created flutter creates a corresponding element for it the first time it encounters the widget which forms an element tree. These elements are pointers which refer to the widget it represents and therefore acts as a link between the widget and render tree. When a widget rebuilds a new instance of the widget is actually created and the element updates to reference the new widget but a new element isn't created.

The Render Tree

For every widget there is also an associated render object which actually draws on the screen. When a widget rebuilds a new render object isn't created, it simply compares the old configuration with the current one and updates the part that has been changed.

In other words the widgets we create or use are simply a means to report to the framework a detailed description of what we want to achieve, and the element tree serves as the middle man that lets the render tree know exactly which widget it is to draw. The widget tree is controlled by us while the other two are based on the widget tree and controlled by flutter.

The most common way to trigger the build method is by calling setstate which marks a widget as "dirty". This simply informs flutter that the widget needs to be rebuild, the rebuild happens when the next refresh occurs which, with flutter running at 60 frames per second is almost instantaneous. When this happens the build method of the widget that called setstate runs again and every widget in the build method gets recreated (i.e. a new instance of every widget in the build method is created and the old ones destroyed). The element tree then updates its reference to refer to the new widgets being created and informs the render tree about the update. it is then left for the render tree to determine what changed by comparing the previous configuration it had with the current one. Only when it finds a difference does the render object redraw the widget with the difference.

How Build Works

Imagine we had a widget tree like this.

class Example extends StatefulWidget {
  @override
  _ExampleState createState() => _ExampleState();
}

class _ExampleState extends State<Example> {
  Color containerColor = Colors.green;
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Example app bar'),
      ),
      body: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        mainAxisAlignment: MainAxisAlignment.spaceAround,
        children: [
          Container(
            height: 50,
            color: Colors.blue,
            child: Text('Container One'),
          ),
          Container(
            height: 50,
            color: Colors.yellow,
            child: Text('Container Two'),
          ),
          GestureDetector(
            onTap: () {
              setState(() {
                containerColor = Colors.indigo;
              });
            },
            child: Container(
              height: 50,
              color: containerColor,
              child: Text('Container Three'),
            ),
          ),
        ],
      ),
    );
  }
}

The first time the build method of this widget runs, flutter creates an element tree with element object for every widget in the build, it also creates a render tree containing render objects which draws the widgets to the screen using configurations like height, color etc.

The third container is wrapped with a gesture detector with an on tap method. This method calls setstate and changes the color value being used by the container. The setstate method marks the entire Example widget as dirty and on the next refresh the build method is called again. This results in a new instance of every widget being created, so new instances the Scaffold, Appbar, Column, GestureDetector and three Containers with three Text widgets are created. The element tree updates its pointers to refer to the newly created instances. The render tree then compares the previous configuration it had with the current one and when it sees that the color used by the third container has changed it then redraws the third container only.

So the fact that setstate is called and new instances of the widget within the build method is created doesn't mean that flutter redraws everything on the screen.

Optimizing Our Code

The fact that every widget in the build method was recreated before the framework could determine what changed is one of the areas we can improve on to improve the overall efficiency of the app. We can do this by separating the part of the widget tree that updates frequently to is own widget with its own build method. For example

class Example extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Example app bar'),
      ),
      body: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        mainAxisAlignment: MainAxisAlignment.spaceAround,
        children: [
          Container(
            height: 50,
            color: Colors.blue,
            child: Text('Container one'),
          ),
          Container(
            height: 50,
            color: Colors.yellow,
            child: Text('Container two'),
          ),
          CustomContainer(),
        ],
      ),
    );
  }
}

class CustomContainer extends StatefulWidget {
  @override
  _CustomContainerState createState() => _CustomContainerState();
}

class _CustomContainerState extends State<CustomContainer> {
  Color containerColor = Colors.green;
  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {
        setState(() {
          containerColor = Colors.indigo;
        });
      },
      child: Container(
        height: 50,
        color: containerColor,
        child: Text('Container three'),
      ),
    );
  }
}

This does exactly the same thing as before but this time when setstate is called only the third container gets recreated and eventually redrawn. Another minor way we can avoid unnecessary recreation of widgets when build runs is by using const when possible. As this implies that the widget would never change and allows Flutter to build the widget only once and never rebuild them. It’s a small performance improvement that adds up in larger apps.

Conclusion

Calling setstate in a really long widget tree just to update a tiny part of the interface isn't a good practice which is why it is advised to group parts that change frequently into separate widgets. Why rebuild an entire widget tree when you can rebuild just a sub part.

In as much as this helps with performance on a larger scale it also helps improve the readability, reusability and maintainability of the code.

I hope you learnt something knew, leave a like and a comment.