The introduction

There are three trees in Flutter that many people are familiar with. The Widget tree is the most familiar one, which is often used during development. Do you know what the other two trees are and how they are built?

The Widget tree

In the development process, widgets are closely related to us. Almost all pages display widgets. Widgets are at the heart of Flutter, the immutable description of the user interface.

In fact, the widget’s function is to describe the configuration data of a UI element, that is, the widget is not the element that is ultimately drawn to the screen, it just describes the configuration of the display element.

There is no explicit concept of a widget tree during code execution. This tree is our description of how widgets are nested during development, because it does look like a tree

abstract class Widget {
  const Widget({ this.key });
  final Key key;
  
  @protected
  Element createElement();/ / comment 1
  
  static bool canUpdate(Widget oldWidget, Widget newWidget) {/ / comment 2
   returnoldWidget.runtimeType == newWidget.runtimeType && oldWidget.key == newWidget.key; }}Copy the code

Widget itself is an abstract class that receives a key. For details on how to use a key, see this article.

  • Note 1

    CreateElement is an abstract method that subclasses must implement. This method creates an Element, so each Element corresponds to a widget object.

  • Note 2

    Determine whether the oldWidget and newWidget are the same widget, and if they have the same runtimeType and key, they are the same widget.

Note that widgets cannot be modified, they can only be recreated, because WDiget is not involved in rendering, it is just a configuration file that tells the rendering layer its style.

The Element tree

The only Element that actually appears on the screen in Flutter is the Element class. That is, the widget only describes the configuration data of the Element, and the widget can correspond to multiple elements. This is because the same widget can be added to different parts of the Element tree. When rendered, each Element corresponds to a widget object.

A UI tree is made up of Element nodes. RenderObject is used to complete the final Layout and render of components. The general process from creation to rendering is as follows: Generate the Element from the widget, then create the corresponding RenderObject and associate it with the Element. RenderObject property, and finally arrange and draw the layout with the RenderObject.

Element represents an instance of a particular location in the widget tree. Most elements have only a single RenderObject, but some elements have multiple child nodes, such as classes that inherit from RenderObjectElement. Such as MultiChildRenderObjectObject. Finally, all of the Element’s RenderObjects form a tree, which we call a rendering tree.

To summarize, the UI system of Flutter contains three trees: the Widget tree, the Element tree and the render tree. The Element tree is generated from the Widget tree, while the render tree depends on the Element tree, as shown in the figure below:

Element class source code

abstract class Element extends DiagnosticableTree implements BuildContext {
  
  Element(Widget widget)
    : assert(widget ! =null),
      _widget = widget;

  Element _parent;

  @override
  Widget get widget => _widget;
  Widget _widget;

  RenderObject get renderObject { ... }

  @mustCallSuper
  void mount(Element parent, dynamic newSlot) { ... }

  @mustCallSuper
  void activate() { ... }

  @mustCallSuper
  void deactivate() { ... }

  @mustCallSuper
  voidunmount() { ... }}Copy the code

Element’s life cycle

  • initial

    The initial state

    _ElementLifecycle _lifecycleState = _ElementLifecycle.initial;
    Copy the code
  • active

    RenderObjectElement mount method
    @override
    void mount(Element? parent, Object? newSlot) {
      super.mount(parent, newSlot);
      / /...
      _renderObject = widget.createRenderObject(this);
      assert(_slot == newSlot);
      attachRenderObject(newSlot);
      _dirty = false;
    }
    Copy the code

    When the fragment calls the element.mount method, The mount method first calls the createRenderObject method of the element widget to create the RenderObject object corresponding to the Element.

    Then call element. AttachRenderObject will element. RenderObject added to render tree slot position (this step is not necessary, generally occur in the element tree structure changes didn’t need to attach.

    The element inserted into the render is in the active state, and when it is in the active state it can be displayed on the screen (it can be hidden).

    super.mount(parent,newslot)
    _lifecycleState = _ElementLifecycle.active
    Copy the code

    When a widget is updated, the updateChild method is called to avoid re-creating the Element to determine whether it can be updated

    @protected
    @pragma('vm:prefer-inline')
    Element? updateChild(Element? child, Widget? newWidget, Object? newSlot) {
      // When there is no new widget and the original widget exists, the original child is removed because it is no longer configured
      if (newWidget == null) {
        if(child ! =null)
          deactivateChild(child);
        return null;
      }
      final Element newChild;
      // There is a child
      if(child ! =null) {
        bool hasSameSuperclass = true;
    	assert(() {
            final int oldElementClass = Element._debugConcreteSubtype(child);
            final int newWidgetClass = Widget._debugConcreteSubtype(newWidget);
            hasSameSuperclass = oldElementClass == newWidgetClass;
            return true; } ());// If the parent control type is the same and the child controls are the same, update it directly
        if (hasSameSuperclass && child.widget == newWidget) {
          if(child.slot ! = newSlot) updateSlotForChild(child, newSlot); newChild = child; }else if (hasSameSuperclass && Widget.canUpdate(child.widget, newWidget)) 
            // If the parent control has the same type and the widget can be updated, update the child
          if(child.slot ! = newSlot) updateSlotForChild(child, newSlot); child.update(newWidget);assert(child.widget == newWidget);
          assert(() { child.owner! ._debugElementWasRebuilt(child);return true; } ()); newChild = child; }else {
          // Can not update, you need to remove the original child, create a new child and add
          deactivateChild(child);
          assert(child._parent == null); newChild = inflateWidget(newWidget, newSlot); }}else {
        // No child, create a new child and add it
        newChild = inflateWidget(newWidget, newSlot);
      }
      return newChild;
    }
    Copy the code

    Weidget. CanUpdate, mainly check whether the type and key are the same. If we need to force the update, we only need to change the key. We do not recommend changing the runtimeType

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

    Transition from inactive to active life cycle state

    In the updateChild method above, if you finally call the inflateWidget() method, you need to change the state from Inactive to active

    @mustCallSuper
    void activate() {
      assert(_lifecycleState == _ElementLifecycle.inactive);
      assert(widget ! =null);
      assert(owner ! =null);
      assert(depth ! =null);
      final boolhadDependencies = (_dependencies ! =null&& _dependencies! .isNotEmpty) || _hadUnsatisfiedDependencies; _lifecycleState = _ElementLifecycle.active;// We unregistered our dependencies in deactivate, but never cleared the list.
      // Since we're going to be reused, let's clear our list now._dependencies? .clear(); _hadUnsatisfiedDependencies =false;
      _updateInheritance();
      if(_dirty) owner! .scheduleBuildFor(this);
      if (hadDependencies)
        didChangeDependencies();
    }
    Copy the code
  • inactive

    Transition from active to inactive life cycle states

    As we can see from the updateChild method above that the new widget is empty and the old widget exists, we call deactiveChild to remove the Child, and then call deactivate to set _lifecycleState to inactive

    @mustCallSuper
    void deactivate() {
      assert(_lifecycleState == _ElementLifecycle.active);
      assert(_widget ! =null); // Use the private property to avoid a CastError during hot reload.
      assert(depth ! =null);
      if(_dependencies ! =null&& _dependencies! .isNotEmpty) {for (final InheritedElement dependency in _dependencies!)
          dependency._dependents.remove(this);
      }
      _inheritedWidgets = null;
      _lifecycleState = _ElementLifecycle.inactive;
    }
    Copy the code
  • defunct

    Transition from inactive to defunct life cycle state

    @mustCallSuper
    void unmount() {
      assert(_lifecycleState == _ElementLifecycle.inactive);
      assert(_widget ! =null); // Use the private property to avoid a CastError during hot reload.
      assert(depth ! =null);
      assert(owner ! =null);
      // Use the private property to avoid a CastError during hot reload.
      finalKey? key = _widget! .key;if (key isGlobalKey) { owner! ._unregisterGlobalKey(key,this);
      }
      // Release resources to reduce the severity of memory leaks caused by
      // defunct, but accidentally retained Elements.
      _widget = null;
      _dependencies = null;
      _lifecycleState = _ElementLifecycle.defunct;
    }
    Copy the code

StatelessElement

Container creates a StatelessElement. The following is a brief description of the call process.

class StatelessElement extends ComponentElement {
  // The widget passed in when created with createElement
  StatelessElement(StatelessWidget widget) : super(widget);

  @override
  StatelessWidget get widget => super.widget as StatelessWidget;

  // The build method called here is our own build method
  @override
  Widget build() => widget.build(this);
}


abstract class ComponentElement extends Element {
  /// Creates an element that uses the given widget as its configuration.
  ComponentElement(Widget widget) : super(widget);

  Element? _child;

  bool _debugDoingBuild = false;
  @override
  bool get debugDoingBuild => _debugDoingBuild;

  @override
  void mount(Element? parent, Object? newSlot) {
    _firstBuild();
  }

  void _firstBuild() {
    rebuild();
  }


  @override
  @pragma('vm:notify-debugger-on-exception')
  void performRebuild() {
   / /...
  }

  @protected
  Widget build();
}

abstract class Element extends DiagnosticableTree implements BuildContext {
  // constructor that accepts a widget argument
  Element(Widget widget)
    : assert(widget ! =null),
      _widget = widget;

  @override
  Widget get widget => _widget;
  Widget _widget;
  
  void rebuild() {
    if(! _active || ! _dirty)return;
      
    Element debugPreviousBuildTarget;
    
    // The performRebuild method is not implemented in the current class
    performRebuild();
  }

  /// Called by rebuild() after the appropriate checks have been made.
  @protected
  void performRebuild();
}
Copy the code

Let’s sort out the process as follows:

  1. When StatelessElement is created, the framework calls the mount method, because StatelessElement does not implement mount, so it calls the mount for ComponentElement.

  2. The _firstBuild method is called in mount for the first build. StatelessElement _firstBuild (StatelessElement)

  3. The _firstBuild method finally calls super._firstBuild(), which is the _firstBuild method for ComponentElement, where rebuild() is called. Because ComponentElement is not overwritten, the rebuild method on Element is ultimately called.

  4. Rebuild will eventually call the performRebuild method on ComponentElement. As follows:

     @override
      @pragma('vm:notify-debugger-on-exception')
      void performRebuild() {
        if(! kReleaseMode && debugProfileBuildsEnabled) Timeline.startSync('${widget.runtimeType}',  arguments: timelineArgumentsIndicatingLandmarkEvent);
        assert(_debugSetAllowIgnoredCallsToMarkNeedsBuild(true));
        Widget? built;
        try {
          assert(() {
            _debugDoingBuild = true;
            return true; } ());StatelessElement (StatelessElement, StatelessElement, StatelessElement)
          built = build();
          debugWidgetBuilderValue(widget, built);
        } catch (e, stack) {
          // catch
        } finally {
          _dirty = false;
          assert(_debugSetAllowIgnoredCallsToMarkNeedsBuild(false));
        }
        try {
          // Finally call the updateChild method
          _child = updateChild(_child, built, slot);
          assert(_child ! =null);
        } catch (e, stack) {
          //....
          _child = updateChild(null, built, slot);
        }
        if(! kReleaseMode && debugProfileBuildsEnabled) Timeline.finishSync(); }Copy the code
    @pragma('vm:prefer-inline')
    Element inflateWidget(Widget newWidget, Object? newSlot) {
      assert(newWidget ! =null);
      final Key? key = newWidget.key;
      if (key is GlobalKey) {
        final Element? newChild = _retakeInactiveElement(key, newWidget);
        if(newChild ! =null) {
          assert(newChild._parent == null);
          assert(() {
            _debugCheckForCycles(newChild);
            return true; } ()); newChild._activateWithParent(this, newSlot);
          final Element? updatedChild = updateChild(newChild, newWidget, newSlot);
          assert(newChild == updatedChild);
          return updatedChild!;
        }
      }
      // Create the corresponding element
      final Element newChild = newWidget.createElement();
      assert(() {
        _debugCheckForCycles(newChild);
        return true; } ());// Call mount
      newChild.mount(this, newSlot);
      assert(newChild._lifecycleState == _ElementLifecycle.active);
      return newChild;
    }
    Copy the code

    The code above finally calls the updateChild method, which is mentioned in the Element lifecycle above.

    The updateChild method determines whether the built needs to be updated or replaced. If it needs to be replaced, it clears the original, creates the corresponding Element for the new built, and finally calls the mount method for the corresponding Element. The Element is not necessarily the StatelessElement, but the Element corresponding to the widget in the build method.

To summarize

From the process analysis above we can see that the whole process is like a loop, with the framework calling mount at the beginning. In mount, performRebuild is finally called. In performRebuild, the build method we implemented is called. After getting the corresponding widget, the element of the widget will be recreated if it needs to be replaced. And call the element’s mount method.

The process is roughly as shown above


RenderObjectElement

Let’s use Flex as an example to see how Element is created.

@override
MultiChildRenderObjectElement createElement() => MultiChildRenderObjectElement(this);
Copy the code

Fiex is created by the superclass SingleChildRenderObjectWidget Element, and is inherited from RenderObjectElement MultiChildRenderObjectElement.

Let’s take a look at how RenderObjectElement is called

class MultiChildRenderObjectElement extends RenderObjectElement {
    
      @override
  void mount(Element? parent, Object? newSlot) {
    // Call super.mount to insert the parent passed into the tree
    super.mount(parent, newSlot);
     // 
    final List<Element> children = List<Element>.filled(widget.children.length, _NullElement.instance, growable: false);
    Element? previousChild;
     // Iterate over all children
    for (int i = 0; i < children.length; i += 1) {
      / / load the child
      final Element newChild = inflateWidget(widget.children[i], IndexedSlot<Element?>(i, previousChild)); children[i] = newChild; previousChild = newChild; } _children = children; }}abstract class RenderObjectElement extends Element {
  @override
  void mount(Element? parent, Object? newSlot) {
    // Insert the parent passed into the tree
    super.mount(parent, newSlot);
      
    // Create a renderObject associated with element
    _renderObject = widget.createRenderObject(this);
    // Insert element.renderObjectz at the specified position in the render tree
    attachRenderObject(newSlot);
    _dirty = false; }}Copy the code

So let’s do a general analysis

  1. The first is the call toMultiChildRenderObjectElementmountMethod, insert the parent passed into the tree,
  2. Then, inRenderObjectElementCreate arenderObjectAnd adds to the specified location of the slot in the render tree
  3. Finally back toMultiChildRenderObjectElementIs iterated over all of the children and is calledinflateWidgetCreates a child and inserts it into the specified slot.

StatefulElement

Compared to the StatelessWidget, there is one more State in the StatefulWidget, where a State is created through createState, as shown below

abstract class StatefulWidget extends Widget {
  const StatefulWidget({ Key? key }) : super(key: key);
  
  @override
  StatefulElement createElement() => StatefulElement(this);

  @protected
  @factory
  State createState(); // ignore: no_logic_in_create_state, this is the original sin
}
Copy the code

You can see from the code above that the Element corresponding to the StatefulWidget is a StatefulElement.

class StatefulElement extends ComponentElement {
  /// Creates an element that uses the given widget as its configuration.
  StatefulElement(StatefulWidget widget)
      : _state = widget.createState(),
        super(widget) {
    assert(state._element == null);
    state._element = this;
    state._widget = widget;
  }
    
  State<StatefulWidget> getstate => _state! ; State<StatefulWidget>? _state;@override
  Widget build() => state.build(this);
} 
Copy the code

The state object is retrieved from StatefulElement by calling Widget.creatEstate

When a StatefulElement is created, the framework calls the mount method in StatelessElement.

andStatelessElementThe difference is:

  • StatelesselementIs by callingwidget.build(this)methods
  • StatefulElementIs by callingstate.build(this)methods

conclusion

From the above analysis, we know the life cycle of Element and how it is invoked. And if you look closely at the three elements in the example above, you will see that the above three elements can be divided into two categories: composition and drawing.

  • Composite classes generally inherit fromStatelessElementorStatefulElementIf you look at their mount methods, you will see that the renderObject is not created and added to the render tree. For example,StatelessWidgetStatefulWidget.Text.Container ,ImageAnd so on.
  • The mount method creates the renderObject and attachthe renderObject to the render tree. Such asColumnSizedBox.TransformAnd so on.

In fact, there is another type, here is a picture borrowed from the big guy:

The element of the proxy class is ProxyElement.

RenderObject

We mentioned above that each Element corresponds to a RenderObject, which we can obtain via element.renderObject. RenderObject is responsible for Layout rendering, and all renderObjects form a Render Tree.

Through the above analysis, we know that at the heart of the tree in the mount method, we directly see RenderObjectElement. Mount () method

@override
void mount(Element? parent, Object? newSlot) {
  super.mount(parent, newSlot);
  _renderObject = widget.createRenderObject(this);
  attachRenderObject(newSlot);
  _dirty = false;
}
Copy the code

After super.mount (inserting the parent into the Element tree), the attachRenderObject method is executed.

@override
void attachRenderObject(Object? newSlot) {
  assert(_ancestorRenderObjectElement == null);
  _slot = newSlot;
  // Query the current most recent RenderObject
  _ancestorRenderObjectElement = _findAncestorRenderObjectElement();
  // Insert the current node's renderObject below the renderObject found above_ancestorRenderObjectElement? .insertRenderObjectChild(renderObject, newSlot);/ /...
}

RenderObjectElement? _findAncestorRenderObjectElement() {
   Element? ancestor = _parent;
   while(ancestor ! =null && ancestor is! RenderObjectElement)
     ancestor = ancestor._parent;
   return ancestor asRenderObjectElement? ; }Copy the code

RenderObjectElement = RenderObjectElement = RenderObjectElement = RenderObjectElement = RenderObjectElement = RenderObjectElement This method finds the RenderObjectElement object closest to the current node and calls insertRenderObjectChild, which is an abstract method. We look at the override logic in both classes

  • SingleChildRenderObjectElement

    void insertRenderObjectChild(RenderObject child, Object? slot) {
      final RenderObjectWithChildMixin<RenderObject> renderObject = this.renderObject as RenderObjectWithChildMixin<RenderObject>;
      renderObject.child = child;
    }
    Copy the code

    In the above code, we find the renderObject of the current RenderObjectElement and pass the child we passed to the renderObject’s child. So this is hanging the passed child on the RenderObject tree.

  • MultiChildRenderObjectElement

    @override
    void insertRenderObjectChild(RenderObject child, IndexedSlot<Element?> slot) {
      final ContainerRenderObjectMixin<RenderObject, ContainerParentDataMixin<RenderObject>> renderObject = this.renderObject;
      assert(renderObject.debugValidateChild(child)); renderObject.insert(child, after: slot.value? .renderObject);assert(renderObject == this.renderObject);
    }
    Copy the code

    After the above code, find renderObject assigned to the ContainerRenderObjectMixin < renderObject, ContainerParentDataMixin < renderObject > > this class, Let’s take a look at this class

    /// Generic mixin for render objects with a list of children.
    /// A generic blend of render objects with a list of subitems
    /// Provides a child model for a render object subclass that has a doubly-linked
    /// list of children.
    /// Provides a submodel for a render object subclass with a bidirectionally linked list of subitems
    mixin ContainerRenderObjectMixin<ChildType extends RenderObject, ParentDataType extends ContainerParentDataMixin<ChildType>> on RenderObject {
    }
    Copy the code

    Generic mixins are used to render objects with a set of child objects. See here, we can know MultiChildRenderObjectElement is can have child list.

    Through the above comments can see an important point, bidirectional linked list. So MultiChildRenderObjectElement child nodes connected via two-way linked list. Insert will eventually call the _insertIntoChildList method as follows:

    ChildType? _firstChild;
    ChildType? _lastChild;
    void _insertIntoChildList(ChildType child, { ChildType? after }) {
      final ParentDataType childParentData = child.parentData! as ParentDataType;
      _childCount += 1;
      assert(_childCount > 0);
      if (after == null) {
        // If after is null, insert into _firstChild
        childParentData.nextSibling = _firstChild;
        if(_firstChild ! =null) {
          finalParentDataType _firstChildParentData = _firstChild! .parentData!asParentDataType; _firstChildParentData.previousSibling = child; } _firstChild = child; _lastChild ?? = child; }else {
        final ParentDataType afterParentData = after.parentData! as ParentDataType;
        if (afterParentData.nextSibling == null) {
          // insert at the end (_lastChild); we'll end up with two or more children 
          // Insert child at the end
          assert(after == _lastChild);
          childParentData.previousSibling = after;
          afterParentData.nextSibling = child;
          _lastChild = child;
        } else {
          // insert in the middle; we'll end up with three or more children
          // Insert into the middle
          childParentData.nextSibling = afterParentData.nextSibling;
          childParentData.previousSibling = after;
          // set up links from siblings to child
          finalParentDataType childPreviousSiblingParentData = childParentData.previousSibling! .parentData!as ParentDataType;
          finalParentDataType childNextSiblingParentData = childParentData.nextSibling! .parentData!as ParentDataType;
          childPreviousSiblingParentData.nextSibling = child;
          childNextSiblingParentData.previousSibling = child;
          assert(afterParentData.nextSibling == child); }}}Copy the code

    Insert the child into the first node when after is null, insert the child into the end, and insert the child into the middle.

    Let’s take an example:

    Column(
    	children: [
    	    SizedBox(.....),
    	    Text(data),
     	    Text(data),
     	    Text(data),
     	 ],
    )
    Copy the code

    Call this method after the first Stack finds Column(RenderObjectElement). Then _firstChild is SizedBox. RenderConstrainedBox is SizedBox.

    The second is Text, we know that Text is a composite type, so it will not be mounted to the tree, through the source query can see that the final Text is RichText. After RichText looks up to Column(RenderObjectElement), it calls this method, passing in two arguments. The first child is the RenderParagraph corresponding to RichText. The second after is the SizedBox’s RenderConstrainedBox. Execute the following code according to the above logic

    final ParentDataType afterParentData = after.parentData! as ParentDataType;
    if (afterParentData.nextSibling == null) {
      // insert at the end (_lastChild); we'll end up with two or more children
      assert(after == _lastChild);
      childParentData.previousSibling = after;
      afterParentData.nextSibling = child;
      _lastChild = child;
    } 
    Copy the code

    . The child childParentData previousSibling pointing to the first node, will be the first to answer the afterParentData. NextSibling point to the child, finally let the child _lastchild execution.

    The same is true later, when the process is finished you get a RenderTree.

conclusion

This paper mainly introduces the construction process of three trees and the life cycle of Elemnt. Although these are rarely used in the development process, they are the gate to the internal world of FLUTTER.

In fact, when writing this article is also a little knowledge, through constant view of the source code, read the blog and so on slowly some understanding. And at the end of the output of this article, there may be some mistakes in the article, if you see it, please put forward the following, thank you!!

Recommended reading

  • This article learns about BuildContext
  • The principle and use of Key

The resources

  • Juejin. Cn/post / 692149…
  • Juejin. Cn/post / 684490…
  • Because Chinese website

If this article is helpful to you, we are honored, if there are mistakes and questions in the article, welcome to put forward