Readers of HTML have heard of the DOM tree, which is composed of each control on the page. The natural nesting of these controls allows it to be represented as a “tree” structure. We can also apply this concept to Flutter.
We can also see that there are relationships in the tree structure of each control in the figure above. For example, we can say that the Text component is a child of the Column component and the Scaffold is a parent of the AppBar component. This hierarchy allows each control to be clearly connected. Hence the tree structure.
In The Flutter, components like Container and Text are widgets, so we call this tree the Widget tree, or the control tree, which represents the structure of the control that we wrote in the Dart code.
However, in the Flutter architecture, the actual Rendering of components on screen is not at the Widget layer, but at the Rendering layer. How do the components that we write in code display through the Rendering layer? Two more trees, the Element tree and the RenderingObject tree, were introduced into Flutter.
What Element is, we can call it another abstraction of widgets. The reader can also think of it as a more practical control, because the controls that appear on our phone screen are not widgets that we write in our code. The components that we use in our code, such as Container and Text, and their properties, are just configuration information for the components that we want to build. When we first call the build() method to display these components on the screen, the Flutter generates the Element for the Widget based on this information, and again, the Element is placed in the corresponding Element tree. In Flutter, a Widget can be reused multiple times to correspond to multiple Element instances, which is what we actually display on the screen.
Another difference between Elements and widgets is that widgets are inherently immutable, requiring reconstruction if they are to be updated. If you want to associate mutable state with widgets, you can use the StatefulWidget. StatefulWidget by using StatefulWidget. CreateState method to create the State object, and expand the to Element and incorporated into the tree;
Here, to further understand the meaning of the above description, we can take a more vivid example. The Widget is the Boss. He writes down the recent strategic deployment, or configuration information, on a piece of paper and sends it to Element, the manager, who sees the detailed configuration information and really gets to work. We also need to pay attention to the fact that the big Boss will change the strategic deployment at any time, and then write it down instead of modifying it on the original paper. In this case, the manager needs to compare the new plan with the old plan and make corresponding updating measures in order to reduce the workload. This is also a major optimization of the Flutter framework layer. Again, Element is a decent manager and won’t get all the work done, so it hires a RenderObject employee to do the heavy lifting for it.
RenderObject does the component layout rendering work in Flutter. It also has a RenderObject tree, also known as a render tree, for rendering matches and layout constraints between components.
Familiar with Flutter in the above three trees, believe that readers will the component rendering process have a clear understanding of it after learning the commonly used components have great help to us, we need to use different perspective to look at what we set up the layout and controls, then we will also further to understand the more the mysteries of the unknown.
Description of component rendering process
From the above, we know that each control in the control tree implements a RenderObject to do the render task, and all renderObjects make up the render tree. The process of rendering the Flutter component is as follows:
The rendering process of Flutter starts with user input, and when a signal is received from the user input, it triggers a progress update of the animation, such as the start animation when we first render, or the move animation when we reuse a single list item while scrolling through the phone screen. Then it is time to start building the view data, in which Flutter creates the three view trees described above.
After that, the view will layout, calculate the size of each part, and then paint to generate the visual data of each view. This part of the task is mainly done by RenderObject. Here, the layout process in the Flutter can be represented by the following figure. After the above construction of the rendering tree, the parent render object will pass the layout constraint information down, and the child render object will return the Size according to its own rendering situation. The Size data will be passed up, and the parent render object will complete the layout process finally.
The final step is to “Rasterize” the composite view data, which is actually a vector description data, to help actually generate pixel by pixel fill data from the previous step. In Flutter, the rasterization step is placed in the Engine layer.
In daily development learning, we only need to configure our Widget tree in the code layer, understand the features and usage of various widgets, and the rest of the work can be handed over to our framework layer to implement.
Element tree detail
Now that we know what controls do and how to use them, these widgets are configured with properties that define their presentation, such as the string to be displayed by the Text component and the content to be displayed by the input field component. Our Element tree records this configuration information. Readers familiar with React may be familiar with the concept of “virtual DOM”, which the operation Flutter embodies. Widgets are immutable, changes to them mean rebuilds, and rebuilds are frequent, and it would take a lot of performance damage to give them more work, so we think of the Widget component as a virtual tree of components, and the actual tree that is rendered on the screen is Elememt. It holds a reference to its Widget, and if its Widget changes, it is marked as a dirty Element, so the next time the view is updated, only the modified content is updated based on that status, thus improving performance.
Every time a control is mounted to the control tree, the Flutter calls its createElement() method to create its corresponding Element. Flutter then places this Element in the Element tree and holds a reference to the control that created it, as shown below:
Control will have its subtree:
The child control also creates the corresponding Element to be placed in the Element tree:
State in an Element
We mentioned the immutability of the Widget, so the Element is mutable. As we mentioned earlier, it is marked as a dirty Element as a state that needs to be updated. Another thing we need to pay attention to is that, The State object of a StatefulWidget is actually managed by an Element, as shown in the following figure.
Widgets in the Flutter are being rebuilt all the time. After each rebuild, the Element takes appropriate measures to determine whether the new control I’m referring to has changed from the old control I referenced before. If it hasn’t changed, it only needs to update, and if it doesn’t, it will be recreated. So what does Element depend on to determine if the control has changed? It compares the Widget’s two properties:
- Component type
- Key of the Widget (if any)
The component type is whether the controls are created by the same class, and the Key is the unique identifier of each control.
Render the tree in detail
We have a general idea of how the three important trees and Element trees in the Flutter work. The third rendering tree is responsible for rendering the layout of the components.
Each node in the render tree is an object inherited from the RenderObject class, which is generated by the RenderObject method in Element or the createRenderObject method in RenderObjectWidget. This object internally provides several properties and methods to help lay out how the components in the framework layer are rendered.
We know that StatelessWidget and StatefulWidget are two classes that directly inherit from widgets. In Flutter, there is another class called RenderObjectWidget that also directly inherits from widgets. Instead of a build method, you can create a RenderObject directly through createRenderObject and place it in the render tree. Controls such as Column and Row inherit indirectly from RenderObjectWidget.
The main attributes and methods are as follows:
- Constraints object that is passed to it from its parent
- The parentData object, whose parent object appends useful information.
- The performLayout method calculates the layout of the rendered object.
- Paint method to draw the component and its children.
RenderObject is an abstract class. Each node needs to implement it before it can actually render. The two most important classes that extend RenderOject are RenderBox and RenderSliver. These two classes are the parent classes for all rendering objects that apply the Box and Silver protocols, respectively. They also extend dozens of other classes that deal with specific scenarios and implement the details of the rendering process, such as RenderShiftedBox and RenderStack.
Layout constraints
Above, when we introduced the component rendering process, we learned that the controls in the Flutter need to perform a Layout before rendering on the screen. The process can be divided into two linear processes: constraints are passed down from the top and layout information is passed up from the bottom. The process can be shown in the following figure.
The first linear procedure is used to pass layout constraints. The parent node passes constraints to each child node, which are rules that each child node must follow during the layout phase. It’s like a parent telling their child, “You have to follow the school rules before you can do anything else.” Common constraints include specifying the maximum and minimum widths of child nodes or the maximum and minimum heights of child nodes. This constraint extends down, and the child component produces a constraint that it passes to its children, all the way down to the leaf.
The second linear process is used to convey specific layout information. After receiving the constraint from the parent node, the child node will generate its own specific layout information according to it. For example, the parent node specifies that my minimum width is 500 unit pixels, and the child node may define its width to be 500 pixels, or any value greater than 500 pixels according to this rule. In this way, once you have your layout information, you tell it to the parent node. The parent node will continue to do this and pass it up to the very top.
Let’s look at what specific layout constraints can be passed in the tree. There are two main layout protocols in Flutter: the Box protocol and the Sliver sliding protocol. Here we take the box protocol as an example to expand the specific introduction.
In the box protocol, the constraint passed by the parent to its children is BoxConstraints. This constraint specifies the maximum and minimum width and height allowed for each child node. The parent node passes in BoxConstraints with Min Width 150 and Max Width 300:
When the child node accepts the constraint, it can obtain the value in the green range in the figure above, that is, the width is between 150 and 300, and the height is greater than 100. After obtaining the specific value, it will upload the value of the specific size to the parent node, so as to achieve the parent-child layout communication.
Customize a Center control
After the update, you can also look at the source code of each component and see how it applies the principles mentioned above.
– 2019.07.03 updates
Now, we can apply the layout constraints mentioned in the previous article to the concepts related to the render tree and define a component similar to the center layout RenderObject that is rendered on the screen.
So we call our custom component CustomCenter:
void main() {
runApp(MaterialApp(
home: Scaffold(
body: Container(
color: Colors.blue,
constraints: BoxConstraints(
maxWidth: double.infinity,
minWidth: 100.0,
maxHeight: double.infinity,
minHeight: 100.0),
child: CustomCenter(
child: Container(
color: Colors.red,
),
),
),
),
));
}
Copy the code
Now let’s implement our CustomCenter:
class CustomCenter extends SingleChildRenderObjectWidget {
Stingy({Widget child}) : super(child: child);
@override
RenderObject createRenderObject(BuildContext context) {
// TODO: implement createRenderObject
returnRenderCustomCenter(); }}Copy the code
CustomCenter inherited SingleChildRenderObjectWidget, indicates that the Widget can have only one child controls, among them, the createRenderObject (…). The RenderObject method is used to actually create and return our RenderObject instance. Our RenderObject is RenderCustomCenter, as follows:
class RenderCustomCenter extends RenderShiftedBox {
RenderStingy() : super(null);
// Override the drawing method
@override
void paint(PaintingContext context, Offset offset) {
// TODO: implement paint
super.paint(context, offset);
}
// Override the layout method
@override
void performLayout() {
// Layout the child elements and pass the layout constraints down
child.layout(
BoxConstraints(
minHeight: 0.0,
maxHeight: constraints.minHeight,
minWidth: 0.0,
maxWidth: constraints.minWidth),
parentUsesSize: true);
print('constraints: $constraints');
// Specify the offset position of the child element
final BoxParentData childParentData = child.parentData;
childParentData.offset = Offset((constraints.maxWidth - child.size.width)/2,
(constraints.maxHeight - child.size.height)/2);
print('childParentData: $childParentData');
// Define the size of your own (CustomCenter). Here select the maximum value of the constraint object
size = Size(constraints.maxWidth, constraints.maxHeight);
print('size: $size'); }}Copy the code
RenderCustomCenter inherits from RenderShiftedBox, which in turn inherits from RenderBox. RenderShiftedBox satisfies the box protocol and provides an implementation of the performLayout() method. We need to lay out our children in the performLayout() method. ??
We are using child.layout(…) The parentUserSize () method is passed two arguments to the layout of the child. The first is the layout constraint for the child, and the second is parentUserSize. If set to false, this parameter means that the parent does not care about the size of the selected child, which is useful for layout optimization. If the child changes its size, the parent does not have to relayout. But in our case, we want to place the child at the center of the parent, so once the Size of the child changes, its Offset changes, so the parent needs to rearrange, so we pass true here.
When the child layout (…). Once done, the child determines its own Layout Details. Then we can set the offset to put it where we want it to be. In our example, it’s centered.
Finally, just as the Child selects a size based on the constraint passed by the parent, we need to select a size for the CustomCenter.
The running effect is as follows:
Build the application view
The entry portion of the Flutter App occurs in the following code:
import 'package:flutter/material.dart'; Void main() => runApp(new MyApp());Copy the code
The runApp function takes a Widget-type object as a parameter, which means that in the concept of the Flutter, only the View exists, and any other logic only serves the data and state changes of the View. There is no ViewController(or Activity). Here’s what runApp does:
void runApp(Widget app) { WidgetsFlutterBinding.ensureInitialized() .. attachRootWidget(app) .. scheduleWarmUpFrame(); } class WidgetsFlutterBinding extends BindingBase with GestureBinding, ServicesBinding, SchedulerBinding, PaintingBinding, RendererBinding, WidgetsBinding { static WidgetsBindingensureInitialized() {
if (WidgetsBinding.instance == null)
new WidgetsFlutterBinding();
returnWidgetsBinding.instance; }}Copy the code
In runApp, the incoming widget is mounted to the root widget. The WidgetsFlutterBinding is actually a singleton that uses mixins for services that use other binding implemented in the framework, such as gestures, base services, queues, drawings, and so on. The method scheduleWarmUpFrame is then called, and as you can see from the method annotation, calling this method will actively build the view data. The benefit of this is that Flutter relies on Dart’s MicroTask for the schedule of the frame data build task, which is “warmed up” for the entire cycle by active calls so that the view data is available for the next VSync signal synchronization. Without waiting for MicroTask’s next Tick.
Then let’s look at the attachRootWidget function and see what it does:
void attachRootWidget(Widget rootWidget) {
_renderViewElement = new RenderObjectToWidgetAdapter<RenderBox>(
container: renderView,
debugShortDescription: '[root]',
child: rootWidget
).attachToRenderTree(buildOwner, renderViewElement);
}
Copy the code
AttachRootWidget handed the widget to RenderObjectToWidgetAdapter the bridge, through the bridge, the Element is created, and at the same time can hold widgets and RenderObject references. Then we know from the above that what happens next is the first view data build.
This part demonstrates that the Flutter application maintains the view data of the entire application through the structure of widgets, Elements, and RenderObjects.
P.S.
I was going to write some articles about the principle of Flutter, but today I found that there are many big guys analyzing the source code, especially I saw the article written by Guo Xiaogu. I hope some of my summaries can help you.
My blog: meandni.com/2019/05/05/…
My Github: github.com/MeandNi/
Welcome to discussion!