Flutter Anatomy is a series of articles about what makes Flutter…Flutter. We try to get under the skin of how Flutter works to get a better understanding about some great features of the framework.
In this article we start to look at how Flutter uses a unique approach to calculating screen layouts which contributes to Flutter’s speed and fluid UI rendering. It is also allows Flutter to use a very simple model of Widget composition, even for complex screens.
With graphical user interface frameworks, layout is the activity that determines the size and position of user interface elements. Size and position is called geometry.
Calculating the geometry of UI elements becomes difficult with the fluid and relative layouts we often see in mobile apps. Often layout managers must make a few passes through the hierarchy of UI elements in order to calculate size & position for both parents and their children — this is known as multi-pass layout.
Flutter, on the other hand uses a linear (and where possible sub-linear) layout. But what does this mean?
Simply, it means that the layout in Flutter is calculated using one pass (down and back up) through a tree of UI Widgets to calculate the geometry for every UI element. (This is not always possible which we will discuss in a later article.)
An important implication is that subsets of the Widget tree can be updated without having to calculate the layouts for the whole screen. This optimisation is what’s meant by sub-linear layout.
If you are familiar with browser repaint and reflow the Flutter approach should already look like a massive optimisation.
How does this work?
The central concept is that a parent Widget applies constraints on how big or small child Widgets are allowed to be. Based on these constraints, child Widgets pass their calculated size back to their parent. Finally the parent determines a child’s position.
There are of course exceptions, but we will get to this in a later article.
Let’s use a simple diagram to visualise this.
The blue parent constrains the yellow children which in turn each constrains its children. These determine the max and min sizes of the child nodes. The size that a widget then calculates based on these constraints are passed back up the tree.
What does a constraint look like?
A constraint is simply a combination of maximum and minimum heights and widths that a child is allowed to be. (We will look at constraints in more detail in another article.)
Assuming the blue widget in the above tree is 100.0 wide and 100.0 high, the the yellow child widget can be a minimum of 0.0 and a maximum of 100.0 for both width and height.
Note that we don’t set X/Y position in Flutter, although some positioning of children is possible for some widgets, e.g. using a Positionied widget with a Stack Widget .
Let’s see this constraint-based approach in action by creating a child widget that is constrained by its parent.
We create a Container (the parent) with width of 100.0 and height of 100.0. The child of the parent will be another Container, but one that wants to be wider than its parent — we set a width of 200.0 and height of 100.0.
The child’s preferred width and height will violate the constraints dictated by its parent. The parent Container will tell the child Container that its maximum width and height can only be 100.0. Based on the Flutter layout algorithm, the child limits its size to w=100.0, h=100.0.
Let’s dive into some code based on this simple example.
The code is trivial and running the app shows what we expect to see — the yellow child Widget is the same size of its parent.
To see a little more about what’s going on, let’s take a look at the render tree.
The Render Tree ( a quick detour)
Wait. What is this ‘render tree’ thing? Let’s take a quick detour to understand what this is.
In Flutter the render tree is the result of calculating the layout of a Widget tree. In other words the render tree contains the low-level UI objects that describe how to draw a widget. What is interesting to us in this article is that each UI object (called a RenderObject) has a geometry — i.e. the size and position of what will be drawn on the screen.
Let’s get back to looking at the render tree for our simple UI.
The Container Widget is actually a composition of two Widgets which is why you will see RenderConstrainedBox and RenderDecoratedBox for each Container. To make it clearer, I’ve marked the render objects respectively in blue (for the parent Container) and yellow(for the child Container).
You can take a peek in the Container Widget source code to see how it works http://bit.ly/2PqGvQq.
What’s relevant to our understanding of Layout in Flutter are the three green arrows. These tell us the following:
- The yellow child Widget has BoxConstraints that match those of the blue parent Widget (i.e. w=100.0, h=100.0).
- The size of the rendered object is w=100.0, h=100.0 — this conforms to the BoxContraints dictated by the parent and not a width of 200.0 as we set in the yellow child Widget. (In other words, there is no hidden overflow.)
- The ‘additionalConstraints’ property keeps a record of the preferred width and height that we set for the child Widget.
Here is a simple visualisation of what’s happening.
The simplicity of this approach hides its significance. Let’s examine some important implications.
1/ Efficient layout of complex screens
Screens with lots of nested Widgets can be laid-out efficiently. Even as your screens become complex, Flutter can (in most cases) calculate the layout by one pass through all the Widgets and back again.
This is why you can use lots of Widgets in your layout and still see fast fluid screens.
2/ Partial layouts during screen updates
Screens are updated to reflect new state (e.g. displaying new data) or to display animations or to respond to input. When this happens Flutter does not have to calculate the entire layout of the screen each time — only the parts of the screen that have changed.
This optimisation is what is referred to as sub-linear layout. It is possible because the constraints of the parent Widget are not affected by an animation or other updates happening in the child Widgets. Therefore Flutter does not need to recalculate the layout of the parent again. This is known as a relayout boundary.
Contrast this with reflow & repaint in browsers where a change deep in the DOM tree can cause changes all the way to the root. HTML developers need to factor this in whereas Flutter developers do not — well…this is almost true, see the next point.
We will look at this in more detail in a later article and also examine exceptions that make partial layouts inefficient.
3/ Mixing Stateless and Stateful Widgets
The approach to have Stateless and Stateful Widgets makes sense in the context of Flutter’s layout mechanics.
Stateless Widgets can provide layout constraints that do not change for a screen. Stateful Widgets can change the data for a Widget or some visual constraints like AnimatedWidget.
The partial layout of a screen means StatefulWidgets can be very efficient. However to see this benefit, we must avoid putting StatefulWidgets as the parent of large parts of our screen.
See http://bit.ly/2VBRRYm for tips on how to optimise layout of children in Stateful Widgets.
How are other UI frameworks different?
It is interesting to take a quick look at how other UI frameworks handle layout . This helps us see how Flutter’s approach is a little different to what you might have seen before.
Let’s draw our simple 2 box layout in three different frameworks.
In a browser, the parent is naturally sized to fit the child. In this case we can see that the parent DIV is pushed out to be 200px wide. Of course it is possible to set the overflow style attribute to ‘hidden’ so that the child DIV is clipped and is visually 100px x 100px.
This layout look as follows.
In iOS, UIViews can ‘contain’ other UIVIew as sub-views which isn’t exactly a parent-child. The key difference is that these views inhabit different layers and the sub-view sits on-top of its parent. This means the sub-view will be 200 wide. Another important difference is that the UIViews have positions — in this case constraints are used to position a View relative to another using anchor constraints.
The layout looks as follows.
The approach to screen layout in Flutter is simple but powerful. Parent Widgets enforce constraints on child Widgets so a single pass through a Widget tree is enough to calculate layout and positioning.
This article looks at a very simple example to show the key layout concepts in Flutter and how this is different from other UI Frameworks.
In following articles we will dig deeper into some of the layout mechanics and how to use this knowledge to improve your Flutter UIs.