Flutter Basics - How Flutter renders the contents on the screen?
The Widget Tree, Element Tree, and Render Tree Explained!
After reading my previous article in this series, you would be wondering that how actually flutter rebuilds widgets. Every widget has its own build method and they are called often! During a complete rebuild, how much of a performance hit do we encounter?
To answer all this we need to understand the element tree, widget tree, and render tree. Along with these we also need to understand how flutter executes the build(){...}
method, how it rebuilds and repaints the screen.
Behind the scenes
Flutter does not redraw/recreate the entire UI on every
build(){...}
method call. We will discuss this in detail further.Flutter aims to give you a 60 FPS (Frames Per Second) application. So, it updates the screen 60 times per second. It means that the screen is repainted by Flutter 60 times per second. This however is not bad because all apps and games run at 60 FPS or more by default.
This would only become inefficient if Flutter would have to recalculate the entire layout 60 times per second!
If Flutter draws something onto the screen for the first time it needs to figure out the position, color, text, etc. of every element on the screen. In short, during the first render, only the flutter needs to configure every single pixel on the screen.
For subsequent repaints/draws, i.e, for refreshes of the UI if nothing changed then Flutter takes the old information that it has already derived previously and paints that on the screen which is super duper fast and very efficient.
Hence, the refresh rate is not the problem, it would only be a problem if Flutter had to calculate everything on the screen with every refresh.
This is what we will discuss in detail here that if Flutter recalculates everything whenever a
build(){...}
method is called.
The Widget Tree
The widget tree is simply all the widgets that you use to build the application, i.e, the code that you write builds up the widget tree.
It is completely controlled by you. You position the widgets within each other nesting them together to build desired layouts.
This tree that we create by our code and is built by Flutter when calling the
build(){...}
method is just a bunch of configuration settings that Flutter takes into account.It is not simply outputted on the screen. Instead, it tells Flutter what should be outputted on the screen. The widget tree rebuilds frequently.
The Element Tree
The element tree links the widget tree, i.e, the configuration that we set up with the actually rendered objects/elements. It rebuilds very rarely.
Element tree is managed differently and does not rebuild with every call to the
build(){...}
method.For every widget in the widget tree Flutter automatically creates an element. It is done when the widget is encountered by the Flutter for the very first time.
Here we can say that an element is an object that is managed in the memory by Flutter which holds a reference to the widget in the widget tree.
The element just holds a reference to the widget(in the widget tree) that holds the configuration in the end.
When Flutter encounters a stateful widget, it creates the element and then also calls the
createState()
method to create a new state object based on the state class.Hence, the state which is an independent object in a stateful widget is connected to both, the element in the element tree and the widget in the widget tree.
The Render Tree
The render tree is the representation of elements/objects which are actually rendered onto the screen.
The render tree also doesn't rebuild frequently!
The element tree is also linked to the render tree. An element in the element tree points to the rendered object that we actually see on the screen.
Whenever Flutter encounters an element that hasn't been rendered previously then it does so by referencing the widget in the widget tree for the configuration, then creates an element in the element tree.
Flutter also has a layout phase where it calculates and derives the space that is available on the screen, dimensions, size, orientation, etc.
It also has another phase that attaches listeners to the widgets so that we can tap into events and so on.
Simply, we can say that the element that hasn't been rendered will be rendered to the screen. The element (in the element tree) then has a pointer to the rendered object(in the render tree) on the screen. It also has a pointer to the widget (in the widget tree) which holds the configuration.
How Flutter executes build(){...}
method
The build(){...}
method is called by Flutter whenever the state changes. There are basically two important triggers that can lead to a rebuild.
One is when the
setState(){...}
method is called in a stateful widget. CallingsetState(){...}
automatically leads tobuild(){...}
method call.Secondly, whenever there is a
MediaQuery
call orTheme.of(...)...
call, the soft keyboard appears/disappears, etc. whenever the data of these changes it automatically triggers thebuild(){...}
method.
Precisely, calling setState(){...}
marks the respective element as dirty. For the next refresh, which happens 60 times per second, Flutter then takes the new configuration as created by the build(){...}
method into account and then updates the screen.
All the nested widgets inside the widget marked dirty, which are also dart classes, new objects are instantiated of all these widgets/dart classes. Hence a new widget tree is created with new instances of all these widgets.
The new widget tree is created because the widget tree is immutable,i.e, you can't change the property of an existing widget but only we can overwrite it with a brand new one. This methodology is heavily enforced by Flutter because it can detect changes more efficiently when an object changes.
An Example
Consider a container that is a stateful widget that is rendered onto the screen. It has a nested child widget which is a text. The text widget is a stateless widget and displays text on the screen based on the state of its parent widget.
import 'package:flutter/material.dart';
class TestWidget extends StatefulWidget {
@override
_TestWidgetState createState() => _TestWidgetState();
}
class _TestWidgetState extends State<TestWidget> {
var state = true;
@override
Widget build(BuildContext context) {
return Container(
color: Colors.green,
child: GestureDetector(
onTap: () {
setState(() {
state = !state;
});
},
child: Text(state ? 'True' : 'False')),
);
}
}
Above you can see that as soon as the state changes the displayed text will change from True
to False
and vice versa. A GestureDetector
is attached as a parent to the Text
widget as its parent which handles the state change by calling the setState(){...}
method.
As soon as the text widget is tapped the overall parent which handles the state, i.e, TestWidget
is marked dirty. A new widget tree is formed when the build method is called by Flutter.
The new widget tree configuration is then compared to the actual content that is rendered onto the screen. Flutter detects that the container color didn't change and only the text inside the Text
widget changed. So, the render tree will not entirely re-render the content but only the text inside the text widget.
This is why Flutter is so much efficient. The element tree is not rebuilt when the build(){...}
method is called. Only the widget tree is rebuilt and then the element tree just updates its reference pointing to the new widget tree. Then it checks if the new configuration of the widgets is available. If that is the case then it passes that onto the render objects so that the changes can be reflected on the screen.
If you want to read more about the rendering logic in flutter then I would recommend having a look at this article from the Flutter official docs.
A Quick Sidenote
If you know that some widget is never going to change even on the widget tree rebuild then you can optimize the build process even more. You can use the const
keyword in front of them which tells Flutter that the respective widget will never change thus making Flutter skip that widget's complete rebuild. For example,
Padding(
padding: const EdgeInsets.all(8.0),
child: GestureDetector(
onTap: () {
setState(() {
state = !state;
});
},
child: Text(state ? 'True' : 'False')),
),
Above we specify const
which skips the rebuild of the Padding
widget. Also, you can optimize the build process by carefully and smartly splitting your bigger widgets into smaller ones.
Conclusion
Well, that was a lot of discussions and a much deeper dive into the internals of Flutter. I hope now you have a better and clearer understanding of what happens behind the scenes. This article will enforce you to write better dart code because in the back of your mind you will now always have an urge to optimize the build process.
I hope you liked this post. In the next article, we will have a look at how to add navigation and multiple screens to a Flutter app. Till then, Adios and Happy Coding👨🏽💻👩💻!