Everything is a widget! Or is it?
You have probably heard this phrase many times. It has become the slogan of Flutter – in Flutter, everything is a widget! But what is a widget and how true is this saying? At first glance, the answer might seem simple: a widget is a basic building block of UI, and everything you see on the screen is a widget. While this is true, these statements don’t provide much insight into the internals of a widget.
The framework does a good job of abstracting those details away from the developer. However, as your app grows in size and complexity, if you don’t follow best performance practices, you may start encountering issues related to frame drop. Before this can happen, let’s learn about the Flutter build system and how to make the most of it.
What is a widget?
For most of our development, we will create widgets that extend StatelessWidget
or StatefulWidget
. The following is the code for these:
abstract class StatelessWidget extends Widget {...} abstract class StatefulWidget extends Widget {...} @immutable abstract class Widget {...}
From the source code, we can see that both of these widgets are abstract classes and that they inherit from the same class: the Widget
class.
Another important place where we see the Widget
class is in our build
method:
Widget build(BuildContext context) {...}
This is probably the most overridden method in a Flutter application. We override it every time we create a new widget and we know that this is the method that gets called to render the UI. But how often is this method called? First of all, it can be called whenever the UI needs an update, either by the developer, for example, via setState
, or by the framework, for example, on an animation ticker. Ultimately, it can be called as many times as your device can render frames in a second, which is represented by the refresh rate of your device. It usually ranges from 60 Hz to 120 Hz. This means that the build
method can be called 60-120 times per second, which gives it 16-8 ms (1,000 ms / 60 frames-per-second = 16 ms or 1, 000 ms / 120 frames-per-second = 8 ms) to render the whole build
method of your app. If you fail to do that, this will result in a frame drop, which might mean UI jank for the user. Usually, this doesn’t make users happy! But all developer performance optimizations aside, surely this can’t be what’s happening? Redrawing the whole application widget tree on every frame would certainly impact performance. This is not what happens in reality, so let’s find out how Flutter solves this problem.
When we look at the Widget
class signature, we see that it is marked with an @immutable
annotation. From a programming perspective, this means that all of the fields of this class have to be final
. So after you create an instance of this class, you can’t mutate any of its fields (collections are different but let’s ignore this for now and return to it in Chapter 4). This is an interesting fact when you remember that the return type of our build
method is Widget
and that this method can be called up to 120 times per second. Does that mean that every time we call the build
method, we will return a completely new tree of widgets? All million of them? Well, yes and no. Depending on how you build your widget tree and why and where it was updated, either the whole tree or only parts of it get rebuilt. But widgets are cheap to build. They barely have any logic and mostly serve as a data class for another Flutter tree that we will soon observe. Before we move on to this tree though, let’s take a look at one special type of widget.
Getting to know the RenderObjectWidget and its children
We have already discussed that when dealing with widgets, we mostly extend StatelessWidget
and StatefulWidget
. Inside the build
method of our widgets, we only compose them like Lego bricks using the widgets already provided by the Flutter framework, such as Container
and TextFormField
, or our own widgets.
Most of the time, we only use the build
method. Less often, we may use other methods such as didChangeDependencies
or didUpdateWidget
from the State
object. Sometimes we may use our own methods, such as click handlers. This is the beauty of a declarative UI toolkit: we don’t even need to know how the UI we compose is actually rendered. We just use the API. However, in order to understand the intricacies of the Flutter build system, let’s think about it for a moment.
How many times have you used SizedBox
to add some spacing between other widgets? An interesting thing about this widget is that it extends neither StatelessWidget
nor StatefulWidget
. It extends RenderObjectWidget
. As a developer, you will rarely need to extend this widget or any other that contains RenderObjectWidget
in its title. The important thing to know about this widget is that it is responsible for rendering, as the name suggests. Each child of RenderObjectWidget
has an associated RenderObject
field. The RenderObject
class is one of the three pillars of the Flutter build system (the first being the widget and the last being the Element
, which we will see in the next section). This is the class that deals with actual low-level rendering details, such as translating user intentions onto the canvas.
Let’s take a look at another example: the Text
widget. Here is a piece of code for a very simple Flutter app that renders the Hello, Flutter
text on the screen:
void main() { runApp( const MaterialApp( home: Text('Hello, Flutter'), ), ); }
We use two widgets here: the MaterialApp
, which extends StatefulWidget
, and Text
, which extends a StatelessWidget
. However, if we take a deeper look inside the Text
widget, we will see that from its build
method, a RichText
widget is returned:
// Some code has been omitted for demo purposes class Text extends StatelessWidget { @override Widget build(BuildContext context) { return RichText(...); } } class RichText extends MultiChildRenderObjectWidget {...}
An important difference here is that RichText
extends MultiChildRenderObjectWidget
, which is just a subtype of a RenderObjectWidget
. So even though we didn’t do it explicitly, the last widget in our widget tree extends RenderObjectWidget
. We can visualize the widget tree, and it will look something like this:
Figure 1.1 – Visual example of a widget tree
Even though you, as a developer, won’t be extending RenderObjectWidget
often, you need to remember one takeaway!
This is important!
No matter how aggressively you compose your widget tree, the widgets that are actually responsible for the rendering will always extend the RenderObjectWidget
class. Even if you, as a developer, don’t do it explicitly, you should know that this is what is happening deeper in the widget tree. You can always verify this by following the nesting of the build
methods.
Let’s sum up what we’ve learned about the widget types:
StatelessWidget and StatefulWidget |
RenderObjectWidget |
|
Function |
Composing widgets |
Rendering render objects |
Methods |
|
|
Extended by developer |
Often |
Rarely |
Examples |
|
|
Table 1.1 – Widget differences
But if widgets are immutable, then who updates the render objects?
Unveiling the Element class
From the createRenderObject
and updateRenderObject
we understand that render objects are mutable. Yet the widgets themselves that create those render objects are immutable. So how can they update anything, if they are recreated every time their build
method is called?
The secret lies within the Widget
API itself. Let’s take a closer look at some of its methods, starting with createElement
:
@immutable abstract class Widget { Element createElement(); }
The first method that should interest us is createElement
, which returns an Element
. The element is the last of the three pillars of the Flutter build system. It does all of the shadow work, giving the spotlight to the widget. createElement
gets called the first time the widget is added to the widget tree. The method calls the constructor of the overriding Element
, such as StatelessElement
. Let’s take a look at what happens in the constructor of the Element
class:
abstract class Element { Widget? _widget; Element(Widget widget) :_widget = widget {...} }
We pass the widget
field as the parameter to the constructor and assign it to the local _widget
field. This way, the Element retains the pointer to the underlying widget, yet the widget doesn’t retain the pointer to the element. The _widget
field of the element is not final
, which means that it can be reassigned. But when? The framework calls the update
method of the Element
any time the parent wishes to change the underlying widget. Let’s take a look inside the update
method source code:
abstract class Element { void update(covariant Widget newWidget) { _widget = newWidget; } }
As we can see, the pointer to the _widget
field is changed to newWidget
, so the old widget is thrown away, yet our element stays the same. But in order for this reassignment to happen and for this method to be called, firstly the canUpdate
method of the Widget
class is called. The canUpdate
method checks whether the runtimeType
and key
of the old and new widgets are the same as follows:
abstract class Widget { static bool canUpdate(Widget oldWidget, Widget newWidget) { return oldWidget.runtimeType == newWidget.runtimeType && oldWidget.key == newWidget.key; } }
Only if this method returns true
, which means that the runtimeType
and key
of the old and new widgets are the same, can we update our element with a new widget. Otherwise, the whole subtree will be disregarded and a completely new element will be inserted in this place.
To better understand the flow of this process, let’s take a look at the following diagram:
Figure 1.2 – Element relationship with the widget
The fact that the element can be updated instead of being recreated is even more important for the performance of RenderObjectWidget,
since it deals with render objects that do the low-level painting. In the update method of RenderObjectElement,
we also call updateRenderObject,
which is a performance-optimized method: it only updates the render objects if there are any changes, and it only updates them partially. That’s why even though there may be many calls of the build method, it doesn’t mean that the whole tree gets completely repainted.
Finally, let’s summarize everything we’ve learned about the Flutter tree system:
Widget |
Element |
RenderObject |
|
Mutable? |
No |
Yes |
Yes |
Cheap to create? |
Yes |
No |
No |
Created on every build? |
Yes |
No |
No |
Used by devs |
Always |
Almost never |
Very rarely |
Relationships |
Every widget has an |
Implements |
Only created by implementers of |
Table 1.2 – Summary of widget, Element, and RenderObject roles
As we have just seen, Flutter has some straightforward yet elegant algorithms that make sure your application runs smoothly and looks flawless. Unfortunately, it doesn’t mean that the developer doesn’t have to think about performance at all, since there are many ways the performance can be impacted negatively if the best practices are ignored. Let’s take a look at how we can support Flutter in maintaining a delightful experience for our users.