Ask questions
I’ve been writing the Flutter interface for a while now and I feel great, especially with the hot loading feature, which saves a lot of time. Declarative programming is also a future trend. Now after basic proficiency, some simple effects can be quickly written out, even if you have not seen the answer can also be found on the Internet, but I feel that there is no in-depth understanding of the bottom, some problems or half-knowledge, these problems such as the following:
- When is the createState method called? Why is it possible to retrieve widget objects directly from state?
- When is the build method called?
- What is BuildContext?
- Do Widget creation changes frequently affect performance? What are the reuse and update mechanisms?
- What is the purpose of creating a Key in a Widget?
Later, I took some time to read some articles about Flutter rendering, focusing on widgets, Elements and RenderObject. Finally, I got some understanding and a clear answer to the above questions. Therefore, I will record them in this article.
The three trees
So we’re going to start with three trees, which is our core, and we’re going to start with a concept.
The Widget tree
The interface we normally write with widgets in declarative form is known as the Widget tree, which is the first tree to be introduced.
RenderObject tree
The Flutter engine needs to render the information of the Widget tree we wrote onto the page so that the human eye can see it. There is of course a rendering tree called RenderObject Tree. This is the second tree. This node handles layout and drawing related matters. There is no one-to-one correspondence between the nodes of the two trees. Some widgets are displayed, and some widgets, such as those inherited from StatelessWidgets & StatefulWidgets, are just a combination of other widgets. These widgets themselves do not need to be displayed, so there are no corresponding nodes in the RenderObject tree.
The Element tree
The Widget tree is very unstable and will execute the build method at every turn. Calling the build method means that all other widgets that the Widget depends on will be recreated. If Flutter resolves the Widget tree directly, Converting it to a RenderObject tree to render directly can be a very performance consuming process, so there must be something to absorb the inconvenience of these changes and cache them. So there’s another Element tree. The Element tree abstracts Widget tree changes (similar to the React virtual DOM Diff). Only the parts that really need to be modified can be synchronized to the real RenderObject tree to minimize the modification of the real rendering view and improve rendering efficiency. Instead of destroying the entire render view tree rebuild.
These three trees, shown below, are at the heart of our discussion.
As you can see from the figure above, there is a one-to-one relationship between the widget tree and the Element tree nodes. Each widget has an Element, but the RenderObject tree does not. Only the widgets that need to be rendered have nodes. The Element tree acts as an intermediate layer, a big steward, with references to both widgets and RenderObjects. When the Widget changes, compare the new Widget to the Element to see if it has the same type and Key as the original Widget. If it does, there is no need to recreate the Element and RenderObject. The RenderObject can be updated with minimal overhead by simply updating some of its properties. When the engine parses the RenderObject, it can also render with minimal overhead if only the properties have changed.
The RenderObject tree is the rendering tree. The RenderObject tree is responsible for calculating the layout and rendering. The Flutter engine renders based on this tree. The Element tree acts as an intermediary, managing the generation of RenderObjects and updates from widgets.
This is a conceptual overview, but let’s take a look at the source code.
Learn about widgets, Elements, and RenderObjects from the source code
Widget
The screenshots of the Widget overview below are from the official website
A Widget describes the configuration information of an Element, which is the core class hierarchy within the Flutter framework. A Widget is an immutable description of a part of the user interface. Widgets can be changed to Elements, which manage the underlying render tree.
With so many widgets, let’s break them down into simple categories. As mentioned earlier, widgets can be rendered and non-rendered. There are multiple children and single children in the renderable Widgets (child or children), and stateless and stateful Widgets in the non-renderable Widgets (StatefullWidget and StatelessWidget). Let’s take a look at four typical Widgets, like Padding, RichText, Container, and TextField. Through reviewing the source code, we can see the inheritance relationship of these several classes as shown in the figure below.
If you go to the Widget class, you can see the following methods
@protected
Element createElement();
Copy the code
Widget is an abstract class that subclasses all Widgets. CreateElement is an abstract method that subclasses Widget and Element. Come to StatelessWidget, StatefulWidget, MultiChildRenderObjectWidget, SingleChildRenderObjectWidget inside we can find the createElement method implementation.
SingleChildRenderObjectWidget
@override
SingleChildRenderObjectElement createElement() => SingleChildRenderObjectElement(this);
Copy the code
MultiChildRenderObjectWidget
@override
MultiChildRenderObjectElement createElement() => MultiChildRenderObjectElement(this);
Copy the code
StatefulWidget
@override
StatefulElement createElement() => StatefulElement(this);
Copy the code
StatelessWidget
@override
StatelessElement createElement() => StatelessElement(this);
Copy the code
As you can see, when creating an Element, you pass this, the current Widget, and then return the corresponding Element, which is inherited from Element, and Element has a reference to the current Widget.
Moving on to the RichText and Padding class definitions, which inherit from RenderObjectWidget, you can see that they both have createRenderObject methods, as shown below
Padding
@override
RenderPadding createRenderObject(BuildContext context) {
return RenderPadding(
padding: padding,
textDirection: Directionality.of(context),
);
}
Copy the code
RichText
@override
RenderParagraph createRenderObject(BuildContext context) {
assert(textDirection ! =null || debugCheckHasDirectionality(context));
return RenderParagraph(text,
textAlign: textAlign,
textDirection: textDirection ?? Directionality.of(context),
softWrap: softWrap,
overflow: overflow,
textScaleFactor: textScaleFactor,
maxLines: maxLines,
strutStyle: strutStyle,
textWidthBasis: textWidthBasis,
locale: locale ?? Localizations.localeOf(context, nullOk: true)); }Copy the code
Both RenderPadding and RenderParagraph ultimately inherit from RenderObject. As you can see from the above source code analysis, there are methods to generate elements and renderObjects in the Widget, so you just need to write the Widget. The Flutter framework will help us generate the corresponding Elements and RenderObjects. But when to call createElement and createRenderObject will be discussed later.
Element
The following description of Element is from the official website
An Element is an instantiated object of a Widget at a specific location in the tree. The Widget is a configuration, and the Element is the ultimate object. 2. An Element is created by calling the Widget’s method while traversing the Widget tree. Element holds the context data for view building and is the bridge between structured configuration information and the final rendering.
All widgets generate their own Element, as shown in the figure below.
First, let’s go inside the Element class, which is abstract and you can see some of the key methods and properties.
/// Typically called by an override of [Widget.createElement].
Element(Widget widget)
: assert(widget ! =null),
_widget = widget;
Copy the code
As you can see from the createElement method in the Widget, this is passed to the _widget in the Element constructor. This means that each Element has a Widget reference inside it. The _widget is defined in Element as follows
/// The configuration for this element.
@override
Widget get widget => _widget;
Widget _widget;
Copy the code
The widget in Element is a get method that returns _widget. The relationship between widgets and Elements is also mentioned again from the comments above. Widgets are Element configurations.
For the Element constructor, StatelessfulElement has some special features, as follows
class StatefulElement extends ComponentElement {
/// Creates an element that uses the given widget as its configuration.
StatefulElement(StatefulWidget widget)
: _state = widget.createState(),
super(widget) { ... Omit assert...assert(_state._element == null);
_state._element = this; . Omit assert... _state._widget = widget;assert(_state._debugLifecycleState == _StateLifecycle.created);
}
/// The [State] instance associated with this location in the tree.
///
/// There is a one-to-one relationship between [State] objects and the
/// [StatefulElement] objects that hold them. The [State] objects are created
/// by [StatefulElement] in [mount].
State<StatefulWidget> get state => _state;
State<StatefulWidget> _state;
}
Copy the code
The StatefulElement constructor also calls the Widget’s createState method and assigns it to _state, which answers the question we asked at the beginning of this article (when is the createState method called?). . The StatefulElement contains references not only to the Widget but also to the State of the StatefulWidget. The constructor also assigns the widget to the _widget in _state. So we can use the widget directly in State to get the corresponding widget of State. It was originally assigned during the StatefulElement constructor. [Bug Mc-108226] – Explains the question in the beginning (why is it possible to get widget objects directly in state?) .
Element also has a key method to mount, as follows
@mustCallSuper
void mount(Element parent, dynamicnewSlot) { ... Omit assert... _parent = parent; _slot = newSlot; _depth = _parent ! =null ? _parent.depth + 1 : 1;
_active = true;
if(parent ! =null) // Only assign ownership if the parent is non-null
_owner = parent.owner;
if (widget.key is GlobalKey) {
final GlobalKey key = widget.key;
key._register(this); } _updateInheritance(); . Omit assert... }Copy the code
The Flutter framework creates the corresponding Element based on the Widget. After the Element is created, it calls the Element’s mount method to mount the Element to the Element tree. CreateElement and mount are automatically called by the Flutter framework and do not need to be manually called by the developer. So we may not be paying attention to these processes. The Element mount method requires subclasses, so let’s look at the ComponentElement mount method.
@override
void mount(Element parent, dynamic newSlot) {
super.mount(parent, newSlot);
assert(_child == null);
assert(_active);
_firstBuild();
assert(_child ! =null);
}
Copy the code
Here look at the source code step by step, found the execution link is as follows: _build () [ComponentElement] -> rebuild() [Element] -> performRebuild() [ComponentElement] -> Build ()【StatelessElement】 Look at the last StatelessElement build() source
@override
StatelessWidget get widget => super.widget;
@override
Widget build() => widget.build(this);
Copy the code
The StatefulElement build() source code is shown below
@override
Widget build() => state.build(this);
Copy the code
As you can see, the mount method for ComponentElement finally executes the build method. StatelessElement differs from StatefulElement in that StatelessElement executes the build method in the Widget, Inside the StatefulElement is the state build method. Therefore, it also solves a problem mentioned at the beginning of the article (when is the build method called?). . You also learned how the StatefulWidget relates to its State.
In addition, we see above that the build method passes the parameter this, which is the current Element, while the build method looks like this when we write the code
@override
Widget build(BuildContext context) {
}
Copy the code
So we know that the BuildContext is actually the Element of the Widget. Look at the definition of Element. This also explains the initial question (what is BuildContext?). .
abstract class Element extends DiagnosticableTree implements BuildContext {}Copy the code
Let’s look at the mount method in RenderObjectElement
@override
void mount(Element parent, dynamicnewSlot) { ... Omit assert... _renderObject = widget.createRenderObject(this); . Omit assert... attachRenderObject(newSlot); _dirty =false;
}
Copy the code
ComponentElement is the Element of a non-rendering Widget, and RenderObjectElement is the Element of a ComponentElement. RenderObjectElement is the Element of the render Widget. The mount method of RenderObjectElement is responsible for executing the build method. The latter mount method basically calls the createRenderObject method in the Widget to generate the RenderObject and then assigns the value to its _renderObject.
RenderObjectElement mount is used to create RenderObject. RenderObjectElement mount is used to create RenderObject.
The Widget class has an important static method that could have been included in the Widget section above, but put it inside Element instead. This is the
/// Whether the `newWidget` can be used to update an [Element] that currently
/// has the `oldWidget` as its configuration.
///
/// An element that uses a given widget as its configuration can be updated to
/// use another widget as its configuration if, and only if, the two widgets
/// have [runtimeType] and [key] properties that are [operator==].
///
/// If the widgets have no key (their key is null), then they are considered a
/// match if they have the same type, even if their children are completely
/// different.
static bool canUpdate(Widget oldWidget, Widget newWidget) {
return oldWidget.runtimeType == newWidget.runtimeType
&& oldWidget.key == newWidget.key;
}
Copy the code
Element has a _widget as its configuration information. When a widget changes or is rebuilt, does Element destroy it and rebuild it, or does it simply replace the old widget with the newly created one? The answer is determined by this method, and the above notes can be translated as follows
Determine whether the new Widget can be used to replace Element’s current configuration information. Element uses a specific widget as its configuration information. If the runtimeType and key are the same as the previous widget, the old widget in Element can be updated with a new widget. If neither widget has an assigned key, it can be updated as long as the runtimeType is the same, even if the children of the two widgets are completely different.
Therefore, we can see that even though the widget tree outside is constantly changed and rebuilt, our Element can remain relatively stable and will not be created repeatedly. Of course, we will not mount and generate RenderObject repeatedly. We only need to update related attributes at a minimum cost, minimizing the performance cost. Widgets themselves are just configuration information, simple objects, and their reconstructions do not directly affect rendering and have very little impact on performance. This addresses the other question mentioned above (do Widget creation changes frequently affect performance? What are the reuse and update mechanisms? .
RenderObject
As the name RenderObject tells us, RendreObject is the object responsible for rendering the view. From the above we know a few points
- Renderobjects and widgets do not correspond one to one. Only widgets that inherit from RenderObjectWidget have renderObjects.
- The method that generates the RenderObject, createRenderObject, is defined in the Widget;
- The createRenderObject method in the widget called when RenderObjectElement executes the mount method;
- RenderObjectElement contains a reference to both the Widget and the RenderObject, acting as an intermediary and managing both sides.
RenderObject displays in Flutter are divided into four stages: layout, drawing, composition and rendering. Layouts and drawing are done in RenderObject. Flutter uses a depth-first mechanism to traverse the tree of rendered objects, determine the position and size of each object in the tree, and draw them on different layers. After drawing, Skia takes care of composition and rendering.
conclusion
The link between Widget, Element, and RenderObject is explained in the source code above. Here’s a quick summary.
When a Flutter traverses the Widget tree, it calls the createElement method in the Widget to generate the Element object of the node that contains the reference to the Widget. Specifically, when StatefulElement is created, use the StatefulWidget createState method to createState and assign the value to the _state attribute in the Element. The current widget is also assigned to the _widget in state. Once an Element is created, the Flutter framework performs a mount method. For non-rendered ComponentElements, the mount method performs the build method in the widget. For RenderObjectElement, mount calls the createRenderObject method in the widget to generate the RenderObject, And assign to the corresponding property in the RenderObjectElement. StatefulElement executes the build method by executing the build method in the state and passing itself in, the common BuildContext.
If the Widget’s configuration data changes, the Element node that holds the Widget is also marked as dirty. During the next drawing cycle, a Flutter will trigger an update to the Element tree. The canUpdate method will determine whether a new Widget can be used to update the configuration of the Element or to regenerate the Element. And updates itself and the associated RenderObject with the latest Widget data. With the layout and drawing complete, Skia takes care of the rest. Bitmaps are synthesized directly from the render tree during VSync signal synchronization and then submitted to the GPU.
Answer the questions at the beginning
- When is the createState method called? Why is it possible to retrieve widget objects directly from state?
A: Flutter calls createElement in the Widget to generate the node’s Element object while iterating the Widget tree. The StatefulWidget’s createState method is also used to createState. The widget is also assigned to the _state property of the Element, and the current widget is also assigned to the _widget in state, where a widget’s GET method retrieves the _widget object.
- When is the build method called?
A: Once an Element is created, the Flutter framework performs a mount method. For non-rendered ComponentElements, the mount method performs the build method in the widget. StatefulElement executes the build method by executing the build method in the state and passing itself in, the common BuildContext
- What is BuildContext?
A: StatefulElement executes the build method by executing the build method in state and passing itself in, the common BuildContext. In short, BuidContext is Element.
- Do Widget creation changes frequently affect performance? What are the reuse and update mechanisms?
A: There is no performance impact. Widgets are simple configuration information and are not directly related to layout rendering. The Element layer determines whether the new and old widgets have the same runtimeType and key to determine whether the previous configuration information can be updated directly, that is, replacing the previous widget without having to create a new Element each time.
- What is the purpose of creating a Key in a Widget?
A: The Key is the symbol of a Widget. When a Widget changes, it can be updated directly by determining the runtimeType and Key of the previous Widget in the Element. To learn more about Key functions, read the article on Flutter rendering and learn about Key functions in the demo