visit
I have been with Flutter for more than 2,5 years and during this time I managed to appreciate and feel its various qualities. Today I would like to share an article by my colleague and Flutter Tech Lead in Surf, Mikhail Zotyev (@mjk), about how exactly Flutter works under the hood. You can also find it in .
Flutter is friendly — you can easily and quickly get used to it if you have experience in any other direction. Personally, I came from game development. Flutter is flexible — it is a powerful framework that will allow you to solve various problems, including those of high complexity.And while it is flexible, it is not complicated, but on the contrary, very simple: with the help of a dozen basic widgets, you can assemble quite decent user interfaces. And the most interesting thing is that most likely you will be able to use it, at first without even thinking about the question: "How does it work?"But in this article, we'll go to the Dart side of this force and analyze how Flutter works under the hood.Describes the configuration for an [Element].Turns out, the widget itself is simply a description of a certain Element.
Widgets are the central class hierarchy in the Flutter framework. A widget is an immutable description of part of a user interface. Widgets can be inflated into elements, which manage the underlying render tree.To sum up, Flutter is all about widgets is the bare minimum you need to know to be able to use Flutter. At the same time, it is surrounded by a number of additional mechanisms that serve the framework.We also learned that:
This paragraph supplements the first point. That is, if we need a mutable configuration, we use a certain entity called State which describes the current state of this widget. The state, however, is associated not with the widget itself but with its element representation.Widgets themselves have no mutable state (all their fields must be final).
If you wish to associate mutable state with a widget, consider using a [StatefulWidget], which creates a [State] object (via [StatefulWidget.createState]) whenever it is inflated into an element and incorporated into the tree.
A given widget can be included in the tree zero or more times. In particular a given widget can be placed in the tree multiple times. Each time a widget is placed in the tree, it is inflated into an [Element], which means a widget that is incorporated into the tree multiple times will be inflated multiple times.Let us summarize what we know about widgets so far:
An instantiation of a [Widget] at a particular location in the tree.
Widgets describe how to configure a subtree but the same widget can be used to configure multiple subtrees simultaneously because widgets are immutable. An [Element] represents the use of a widget to configure a specific location in the tree. Over time, the widget associated with a given element can change, for example, if the parent widget rebuilds and creates a new widget for this location.Widgets describe how to configure a part of a user interface. But, as we already know, one and the same widget can be included in the tree multiple times at several locations. Each of these locations is represented by a corresponding element. A widget associated with an element may change over time. This means that elements are more resilient and continue to be used, needing only to update their associations.This is a fairly rational solution. Widgets are an immutable configuration that simply describes a particular part of the interface, thus, they have to be pretty lightweight. Elements responsible for handling objects are significantly heavier and do not get rebuilt unless it is necessary.In order to understand how it works, let us take a look at the element’s lifecycle:
As we can see from the class declaration, the element implements the BuildContext interface. BuildContext is an entity that handles the location of a widget in the widget tree, as stated in its documentation. It is almost identical to the element description. This interface is used to avoid direct manipulation of elements but at the same time give access to the necessary context methods. For example, the findRenderObject method allows you to locate this element’s render object in the tree.
The last class we will look into is RenderObject. As the name implies, it is a render object. RenderObject has a parent as well as a slot where the parent can store child-specific data, for example, this object’s position. This object is responsible for implementing the basic layout and paint protocols.
RenderObject does not define a child model. It can consist of zero, one, or more children. On top of that, it does not define a coordinate system. You are free to choose from Cartesian coordinates, polar coordinates, or whichever system suits your needs. There are also no restrictions on layout protocols. You can decide either to adjust the width and height of the layout, or constrain the size of the layout, or set the size and position of the child. If needed, the children can use their parent’s data.
Let us put ourselves in Flutter’s shoes for a moment. Say, we are given this particular widget
tree.
Flutter: Hey, Padding, I need your element.
Padding: Sure thing, here’s SingleChildRenderObjectElement.
Flutter: That’s where you’ll be staying, element, make yourself at home.
SingleChildRenderObjectElement: It’s all good, guys, but I need RenderObject.
Flutter: Padding, how am I supposed to render you?
Padding: There you go, one RenderPadding, as requested.
SingleChildRenderObjectElement: Great. I’m on it!
Flutter: Who’s next? StatelessWidget, now you give me an element.
StatelessWidget: Here’s your StatelessElement.
Flutter: StatelessElement, you’ll be subordinate to SingleChildRenderObjectElement. This is your desk, get to work.
StatelessElement: Ok.
Flutter: RichText, may I see your element please?
RichText hands their MultiChildRenderObjectElement over.
Flutter: MultiChildRenderObjectElement, here is your place, get to work.
MultiChildRenderObjectElement: I can’t work without a render.
Flutter: RichText, we need a render object.
RichText: Here’s RenderParagraph.
Flutter: RenderParagraph, you’ll be receiving instructions from RenderPadding. MultiChildRenderObjectElement will be supervising your work.
MultiChildRenderObjectElement: I’m all set now and ready to start.
You’re probably wondering why there isn’t a render object for StatelessWidget since we have stated that elements associate configurations with rendering? Let us take a closer look at the basic implementation of the mount method.
void mount(Element parent, dynamic newSlot) {
assert(_debugLifecycleState == _ElementLifecycle.initial);
assert(widget != null);
assert(_parent == null);
assert(parent == null || parent._debugLifecycleState == _ElementLifecycle.active);
assert(slot == null);
assert(depth == null);
assert(!_active);
_parent = parent;
_slot = newSlot;
_depth = _parent != null ? _parent.depth + 1 : 1;
_active = true;
if (parent != null)
_owner = parent.owner;
if (widget.key is GlobalKey) {
final GlobalKey key = widget.key;
key._register(this);
}
_updateInheritance();
assert(() {
_debugLifecycleState = _ElementLifecycle.active;
return true;
}());
}
We will not find a render object being created here. Instead, the element implements BuildContext which contains the findRenderObject method. This method leads us to the next getter:
RenderObject get renderObject {
RenderObject result;
void visit(Element element) {
assert(result == null);
if (element is RenderObjectElement)
result = element.renderObject;
else
element.visitChildren(visit);
}
visit(this);
return result;
}
In the base case, elements may not create a render object, it is something that RenderObjectElement and its descendants do. However, the element must have a child containing a render object at some level of nesting.
It would appear too complicated, having to deal with three trees and different sets of responsibilities. But this is precisely what Flutter’s performance is built upon. Widgets are immutable configurations, therefore they are often being rebuilt. At the same time, they are pretty lightweight, so it does not affect performance. As for heavy elements, Flutter tries reusing them as much as possible.Consider this example.The text is displayed in the center of the screen. The code will look like this:body: Center(
child: Text(“Hello world!”)
),
Now we have a new widget tree. We mentioned that Flutter tries to make the most of the existing elements. Let us take a look at another method in the Widget class with a telling name canUpdate.
static bool canUpdate(Widget oldWidget, Widget newWidget) {
return oldWidget.runtimeType == newWidget.runtimeType && oldWidget.key == newWidget.key;
}
Apart from having type and key, a widget is a description and a configuration. This means that parameter values needed for displaying might have changed. That is why the element must initiate updates of the render object after updating the association with the widget. Nothing changes for Center here. Let us continue examining the widgets.
Again, type and key suggest that there is no point in rebuilding the element. The text is a descendant of StatelessWidget and it does not have a direct display object.@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: _isFirst ? first() : second(),
),
floatingActionButton: FloatingActionButton(
child: Text("Switch"),
onPressed: () {
setState(() {
_isFirst = !_isFirst;
});
},
),
);
}
Widget first() => Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
"test",
style: TextStyle(fontSize: 25),
),
SizedBox(
width: 5,
),
Icon(
Icons.error,
),
],
);
Widget second() => Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
"one more test",
style: TextStyle(fontSize: 25),
),
Padding(
padding: EdgeInsets.only(left: 5),
),
Icon(
Icons.error,
),
],
);
As we can see, Flutter only rebuilt RenderPadding and reused the rest.
Here is another example, in which we will change the levels of nesting, thus changing the structure drastically.Widget second() => Container(child: first(),);
Although the tree appears to be the same, the elements and render objects have been rebuilt. This happened because Flutter compares by level. It does not matter that the tree remained mostly unchanged. This part was filtered out when Container and Row were being compared. However, this can be avoided If we use GlobalKey. Let us add this key to Row.
var _key = GlobalKey(debugLabel: "testLabel");
Widget first() => Row(
key: _key,
…
);
Flutter Code
«How Flutter renders Widgets» Andrew Fitz Gibbon, Matt Sullivan
Also published on: