One, the introduction

There are three trees in Flutter: Widget tree, Element tree and RenderObject tree. When the application starts, Flutter traverses and creates all the widgets to form the Widget Tree. Corresponding to the Widget Tree, Flutter creates each Element object by calling createElement() on the Widget. Form Element Tree. Finally, call Element’s createRenderObject() method to create each Render object, forming a Render Tree. An Element is the Widget’s instantiated object at a specific location in the UI tree. Most elements have only a single renderObject, but some elements have multiple child nodes, such as classes that inherit from RenderObjectElement. Such as MultiChildRenderObjectElement. Finally, all elements’ renderObjects form a Tree, which we call a “Render Tree”. To summarize, we can think of Flutter’s UI system as consisting of three trees: Widget tree, Element tree, and render tree. Their dependencies are to generate the Element tree from the Widget tree and the RenderObject tree from the Element tree, as shown below:

This tree structure is similar to the DOM tree in HTML, as shown in the following figure for the default counter application:

In flutter, components such as Container and Text belong to widgets. Therefore, the Widget tree, also known as the control tree, represents the structure of the widgets we wrote in the DART code. Element is another abstraction of a Widget. Components like Container, Text, and so on that we use in our code and their properties are simply configuration information for the components we want to build, and when we first call build() to display them on the screen, A Flutter will generate the Widget’s Element based on this information. The Element will also be placed into the corresponding Element tree. RenderObject renders the layout of the components in Flutter. It also has a corresponding RenderObject tree (also called a rendering tree) for rendering tie-ups and layout constraints between components.

Second, the Widget tree

Widgets, the core of Flutter, are immutable descriptions of the user interface. The Widget’s function is to “describe the configuration data of a UI element,” which means that the Widget does not actually represent the display element that will eventually be drawn on the device screen, but rather describes the configuration data of the display element. Just as Flutter’s slogan is Everything’s a Widget, developing applications with Flutter is writing widgets. The canUpdate method of a Widget decides to update the corresponding Element of the Widget by comparing the runtimeType and key properties of the new Widget with those of the old Widget.

static bool canUpdate(Widget oldWidget, Widget newWidget) {
    return oldWidget.runtimeType == newWidget.runtimeType
      && oldWidget.key == newWidget.key;
  }
@protected
  Element createElement(a);
Copy the code

Third, the Element tree

In fact, the class in Flutter that really represents the Element displayed on the screen is Element, the Widget is just a configuration of the UI Element, and one Widget can correspond to multiple elements. An Element is the Widget’s instantiated object at a specific location in the UI tree. Most elements have only a single renderObject, but some elements have multiple child nodes, such as classes that inherit from RenderObjectElement. Such as MultiChildRenderObjectElement. The Widget is immutable, and its changes mean that it needs to be rebuilt, which is very frequent. If we assign more tasks to it, the performance will be greatly damaged. Therefore, we treat the Widget component as a virtual component tree, and the tree that is actually 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 that the next time the view is updated, only the modified content is updated based on this state, thus improving performance.

Element’s life cycle is as follows:

  1. The Framework calls widget. createElement to create an Element instance called Element

  2. The Framework calls Element. mount(parentElement,newSlot) The mount method first calls the createRenderObject method of element’s Widget to create the RenderObject associated with the Element. Then call element. AttachRenderObject method will element. RenderObject added to the render tree slot the location specified (this step is not necessary, generally occur in the element tree structure changes didn’t need to attach). Elements inserted into the render tree are in the “active” state, and when they are in the “Active” state, they are displayed on the screen (and can be hidden).

  3. When the configuration data of a parent Widget changes and its state. build returns a different Widget structure than before, you need to rebuild the corresponding Element tree. In order to reuse an Element, we try to see if we can reuse an Element from the same location in the old tree before rebuilding it. Each Element node calls the canUpdate method of its Widget before updating it. If true, the old Element is reused. The old Element is updated with the new Widget configuration data, or a new Element is created. Widget.canUpdate checks whether the runtimeType and key of the newWidget and oldWidget are equal, and returns true if they are equal, false otherwise. According to this principle, when we need to force updates to a Widget, we can avoid reuse by specifying a different Key.

  4. When an ancestor Element decides to remove an Element (for example, if the Widget tree structure changes and the Widget corresponding to the Element is removed), the ancestor Element calls the deactivateChild method to remove it. After removal, element.renderObject will also be removed from the render tree, and the Framework will call element.deactivate, which changes the Element state to inactive.

  5. Elements in the inactive state will no longer be displayed on screen. To avoid repeatedly creating or removing a particular element during an animation execution, an element in an inactive state will remain in its inactive state until the last frame of the animation ends. If it does not return to the active state after the animation ends, The Framework calls its unmount method to remove it completely, and the Element’s state is Defunct, and it will never be inserted into the tree again.

  6. If an Element is to be re-inserted into another location in the Element tree, such as an Element or its ancestor having a GlobalKey (for global reuse of elements), the Framework removes the element from its existing location and then calls its Activate method. And reattach its renderObject to the render tree.

The life cycle of the Element tree is shown below:

RenderObject tree

4.1 introduction

Each node in the render tree is an object inherited from the RenderObject class. It is generated by the renderObject in Element or the createRenderObject method in RenderObjectWidget, which internally provides properties and methods to help components in the framework layer lay out renderings. RenderObject is used to lay out and draw application interfaces, storing information such as element size and layout. Instantiating a RenderObject is very energy-intensive. The main properties and methods of the RenderObject are as follows:

  • The constraints passed to the constraints object from its parent.

  • The parentData object, whose parent object appoints useful information.

  • The performLayout method calculates the layout of this render object.

  • The paint method draws the component and its children.

4.2 Layout Process

Controls in Flutter need to perform a Layout operation before rendering on the screen. It can be specifically divided into two linear processes:

  1. Pass constraints from the top down.

This process is used to pass layout constraints. The parent node passes constraints to each child node, which are the rules that each child must follow during the layout phase. Common constraints include specifying the maximum and minimum width of child nodes or the maximum and minimum height of child nodes. The constraint extends down, and the child component creates the constraint to pass on to its children, all the way to the leaf node.

  1. Pass layout information from the bottom up.

This 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 based on it. For example, if the parent node defines my minimum width as 500 pixels, the child node may define its width as 500 pixels, or any value larger than 500 pixels. In this way, after you have determined your layout information, you tell the parent node that information. The parent node also continues this operation up to the very top. The process can be shown in the following figure:

Flutter has two main layout protocols: the Box Box protocol and the Sliver slide protocol. In RenderBox, there is a size property that holds the width and height of the control. The layout of the RenderBox is implemented by passing BoxConstraints objects down the component tree. The BoxConstraints object can limit the maximum and minimum width and height of the child nodes, which must comply with the constraints given by the parent node. During the layout phase, the parent node calls the layout() method of the child node. The layout method takes two arguments. The first is constraints, which is the size limit that the parent node can place on the child node, depending on the parent node’s layout logic. The other parameter is parentUsesSize, which is used to determine relayoutBoundary. This parameter indicates whether changes in the layout of the child node affect the parent node. If true, the parent node will be marked as needing to be rearranged when the layout of the child node changes. If the layout of the child node changes, the parent node will not be affected.

4.3 Drawing Process

RenderObject can use the paint() method to do the concrete drawing logic. The process is similar to the layout process. Subclasses can implement the paint() method to do their own drawing logic.

void paint(PaintingContext context, Offset offset) {}Copy the code

Canvas can be retrieved by context.canvas, and then canvas API can be called to achieve specific drawing logic. If a node has children, in addition to doing its own drawing logic, it also calls the drawing method of the children through the paintChild() method. Paint () > paintChild() > paint()… .

Five, why need three trees?

Answer first: The purpose of using three trees is to reuse elements as much as possible.

Reusing an Element is important for performance because it has two critical pieces of data: a Stateful widget state object and the underlying RenderObject. This advantage may not be realized when the structure of the application is simple, but once the application becomes complex and the number of elements that make up the page increases, the cost of recreating three trees is high, so updates need to be minimized. When Flutter can reuse elements, the logical state information of the user interface is unchanged and the previously calculated layout information can be reused, avoiding traversing the entire tree.

Reference:

book.flutterchina.club/

Juejin. Cn/post / 684490…

zhuanlan.zhihu.com/p/128469011

www.jianshu.com/p/096a38a24…