The Flutter Forest — Demystifying Flutter trees
Before you continue!
This tutorial considers that you have a running Flutter installation on your PC or Mac and have a basic understanding of the Flutter framework. If that’s not the case, I encourage you to take a look at this article first: Welcome to Flutter! — Widgets
If it’s a forest, let´s go back to the roots!
Every Flutter developer is familiar with widgets, but a real Flutter developer knows what happens underneath. Widgets are placed on top or around others to build complex screens, creating a tree of widgets.
However, as with every declarative framework, the UI code you write is merely an expression of desire for what you want to see on screen, never thinking about how. So, how does the widget tree get rendered on your device’s screen?
Under the hood, the Flutter framework has three different trees responsible for app rendering and a bunch of other interesting stuff:
- Widget Tree
- Element Tree
- Render Object Tree
Note from the author
There is even a fourth one, the Semantics Tree, but let’s keep that for another occasion.
These trees are closely related, as you can see in the diagram below. In this article, we are going to deep dive into what each one does and how it relates to the rest.
Even though most of the time you’re going to work only with the widget tree, it’s essential to know about the rest of them to understand Flutter’s high performance and how we can create more efficient code as well.
For example, we all know that a change in the state of a Stateful Widget triggers a complete rebuild of the widget tree beneath it. But do all the widgets get rebuilt? That’s not how Flutter works, so… How is it managed internally?
Widget Tree
This is the tree you build / code by nesting smaller widgets to form a larger one. Let’s say you have a Container()
widget and a Text()
widget as a child. In this case, your Widget tree will have Container
as the root and Text
as its leaf (child). Thinking of a bigger structure, you may encounter a much bigger widget tree, like:
Note from the author
To be extremely pragmatic, actually in Flutter there’s no Widget Tree. That wording only exists to make it easier for developers to understand how Flutter works underneath. The chain of widgets gets translated into a tree of Elements.
Element Tree
Each widget in your Widget Tree has a corresponding element in the Element Tree. In fact, the Flutter framework creates an element in the Element Tree for each widget by using the createElement()
method.
An Element
represents the use of a widget to configure a specific location in the tree:
Each Element
has a parent and, potentially, a child, thus effectively creating a tree of elements.
Note from the author
Do you remember theBuildContext
? It’s an interface that’s actually implemented in the… (drum roll)Element
class. As theBuildContext
is nothing else but the Element itself, then, that’s the reason why you can travel through the whole “tree of Widgets” by using theBuildContext
!Remember how you get the current theme in a very deep Widget:
Theme.of(context)
…
Like widgets, elements can be either Stateless Elements or Stateful Elements, depending on the widget they’re referring to. A StatelessElement
only refers to a StatelessWidget
, and a StatefulElement
holds references to a StatefulWidget
and its State
.
The Element
could change if the parent widget rebuilds and creates a new widget for this location. But if the new widget is the same type as the previous one, itElement
can be reused. So here’s the core idea behind Flutter’s performance!
To update the UI with new data, you call the setState()
method, updating the State
object. Under the hood, that object will mark the Element as dirty, causing the UI to be updated by calling the build()
method of the associated Widget
again.
Cheat code #1
Always use const constructors whenever possible when building or instantiating widgets. This helps Flutter to rebuild only widgets that should be updated, marking const widgets as not needed to do the computation again.See this article for a performance analysis of using the
const
keyword.
Taking the previous image as an example, if the parent widget mutates the content of the Text
widget to be “Trip B” but the rest of the widgets’ content remains unaltered, the Element Tree will only change the Element
corresponding to the Text
widget, as that’s the only changing widget. As the new widget is of the same type as the previous one, the existing Element
one will be reused to keep the reference to the new Widget
. More information here.
Why does Flutter create a new instance of the Text
in that case? Because each Widget
is immutable:
@immutable
abstract class Widget extends DiagnosticableTree {
const Widget({this.key});
final Key? key;
…
}
Render Tree
This one is the most important tree, as it’s the one that is visible on the screen.
According to Flutter’s official documentation, “the render tree is a data structure that stores the geometry of the user interface”. For each element in the ElementTree, Flutter creates a RenderObject
using the createRenderObject()
method.
RenderObjects
store basic details of widgets like their position, basic layout, and paint protocols, among many others. However, it doesn’t define how many children a parent will have or where they will be positioned. Hence, when the Flutter framework draws your UI, it looks at the Render Object Tree because that’s the one that controls all the rendering properties of each widget. Taking this into consideration, re-instantiating this tree is costly.
Don’t panic! Most Flutter developers do not render objects directly but instead manipulate the render tree indirectly by using widgets.
But how do RenderObjects
get painted on your device’s screen? Cross-platform frameworks typically create an abstraction layer over the underlying native Android and iOS UI rendering libraries, thus providing native UI components. However, this abstraction may add significant overhead, resulting in a lower overall performance of the app compared to pure native code. To overcome this issue, Flutter’s rendering pipeline minimizes those abstractions by embedding its own copy of Skia (or Impeller) as part of the engine and controlling the UI rendering pixel by pixel, achieving higher drawing performance compared to other cross-platform frameworks. You can learn more about the rendering process in the official Flutter documentation or find more resources at the end of this article.
Swinging through the trees
The whole widget rendering process can be summarized in these steps:
- When you run a Flutter app, the main function calls the
runApp()
function. - That function takes the given
Widget
and makes it the root of the widget tree. - Flutter processes through all the widgets and each corresponding
Element
is mounted. - For each
Element
in the tree, Flutter creates aRenderObject
, thus creating a Render Object Tree. - Each
RenderObject
knows how to size and paint the linkedWidget
and listens for input and hit-testing.
Note from the Author
If you want to take a deeper look into all of this, I’d encourage you to start by reading this article.
It’s important to note that, in 95% of the cases, you’ll interact only with the Widget tree in your daily development. So you’ll probably be asking: “Why should I mind about all of this?”. For two main reasons, actually:
- To improve your app’s overall performance.
- To avoid common bugs due to not knowing this (4% of the cases).
The missing 1% involves working with “low-level” libraries, involving touching code to draw/render views on screen.
Boost your Flutter app’s performance.
After all this reading, let’s take advantage of all this knowledge. This section will be a big Cheat Code #2, so take notes!
Prefer Widgets over Functions
At first glance, splitting long build methods into small functions makes perfect sense. But in doing so, we could face two problems:
- Every time the root widget gets rebuilt, all of the inner functions will be called as well.
- If some inner widget needs to be updated, the whole widget will be marked as dirty.
Thus not making use of the advantages of Flutter’s selective build()
method calls from the Widget Tree structure. Not to mention the great advantage of using the const
keyword to improve the Element Tree management (as stated earlier in this article).
Avoid holding many states in your StatefulWidget.
Think about Flutter’s demo counter app. It has a HomePage
widget with a full-featured Scaffold
and a counter in the center of the screen. Every time you call the setState()
method, you’re refreshing the entire Scaffold
!
You can avoid this by using a state management package like flutter_bloc, riverpod, mobx, or the one of your choice. But there’s also a built-in class really useful in this case: ValueNotifier
:
class _MyHomePageState extends State<MyHomePage> {
final _counterNotifier = ValueNotifier<int>(0);
void _onPressed() {
_counterNotifier.value += 1;
} @override
void dispose() {
_counterNotifier.dispose();
super.dispose();
} @override
Widget build(BuildContext context) {
return Scaffold(
...,
body: Center(
child: ValueListenableBuilder(
valueListenable: _colorNotifier,
builder: (_, value, __) => Text(value)
),
),
);
}
}
This way, when you update the _counterNotifier
Flutter will only rebuild the widgets inside the ValueListenableBuilder
.
Rule of Thumb #1
Look for a state management package of your choice, as they provide better state management than stock Flutter resources and some extra powerful tools.
Prefer “builder” methods for Lists
Now that you’re an expert on Flutter’s trees, you’ll get this pretty easily. This topic is about creating more efficient lists by just building what’s strictly necessary.
Suppose you have a list of 100 shops, and only 4 or 5 of them actually appear on screen at a given time. Why would you build the whole Widget Tree, along with the rest of the inner trees? Especially the heavy-lifting Render Object tree!! That’s why the ListView
widget has a ListView.builder
method, which creates and mounts Elements
only for the visible items.
Rule of Thumb #2
Always prefer lazy over static. Rephrasing a famous quote from Bill Gates: “I choose a lazy person to do a hard job. Because a lazy person will find an efficient way to do it.”
This tip is even available in Flutter’s official best practices.
Don’t be afraid of using Keys
For the majority of use cases, you will not need to specify keys for your widgets. But if you want to preserve the state of a widget when moved around in your Widget Tree, you need a key.
The relation between the widget of the Widget Tree and the element of the Element Tree is established by the key. When no key is specified, the type of the widget is used to determine the key. This could lead to a very famous issue in mutable lists: when removing, adding, reordering, or dragging a widget, you would like to preserve its state; if no key is specified, then the state of one or more items will be inconsistent.
Rule of Thumb #3
I should use keys… In a dynamic list of widgets. Or when you think the framework would need your assistance to identify which widget needs updating.
I should put the key… At the top of the widget subtree with the state you need to preserve!
Rule of Thumb #4
I don’t need keys…
The majority of the time, they are not needed. If the entire widget subtree in your collection is stateless, keys aren’t needed. In fact, Stateless should be keyless.
You can refer to this Medium article for more information and to this one for some examples.
Let’s recap!
Flutter is a powerful framework, but it is also complex. By understanding how this framework works under the hood, you can unlock its full potential and write better code.
Understanding how the Widget Tree works and how it interacts with the other trees will help you write more efficient and less error-prone code.
So… What’s next?
Below, you can find some interesting articles about all these topics, and you can also stay tuned to my new articles!
Resources
- Inside Flutter
- Flutter widgets in depth
- Flutter internals
- How Flutter renders the UI
- Exploration of the Flutter rendering mechanism
- A pragmatic guide to BuildContext in Flutter
- Deep dive into Flutter — Trees!
- How to improve the performance of your Flutter app
- Building performant Flutter widgets
- Keys! What are they good for?
- Elements, Keys and Flutter’s performance
- Concept of key in widgets