The Flutter Forest — Demystifying Flutter trees

Matias Silveiro
Globant
Published in
10 min readOct 25, 2023
Photo by Daniel Peters on Unsplash

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:

Image from Flutter’s official documentation

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:

Image from https://flutter-ko.dev/resources/architectural-overview

Each Element has a parent and, potentially, a child, thus effectively creating a tree of elements.

From Flutteris’ Flutter internals

Note from the author
Do you remember the BuildContext? It’s an interface that’s actually implemented in the… (drum roll) Element class. As the BuildContext is nothing else but the Element itself, then, that’s the reason why you can travel through the whole “tree of Widgets” by using the BuildContext!

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.

From Deep dive into Flutter — Trees!

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.

From Flutteris’ Flutter internals

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:

  1. When you run a Flutter app, the main function calls the runApp() function.
  2. That function takes the given Widget and makes it the root of the widget tree.
  3. Flutter processes through all the widgets and each corresponding Element is mounted.
  4. For each Element in the tree, Flutter creates a RenderObject, thus creating a Render Object Tree.
  5. Each RenderObject knows how to size and paint the linked Widget and listens for input and hit-testing.
From CodeMagic’s A pragmatic guide to BuildContext in Flutter

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

From Flutter Performance Tips

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.

Losing state consistency due to matching by type instead of by key (from Flutter’s official documentation)

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!

Matching widget and element state by keys (from Flutter’s official documentation)

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.

A Stateless widget doesn’t hold any state (from Flutter’s official documentation)

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

--

--

Matias Silveiro
Globant
Writer for

Electronic Engineer & Mobile Developer (Android/iOS native, and now learning Flutter!). Currently working @ Globant