Wikipedia describes Flutter as an open source UI software Development Kit created by Google. Flutter is a UI-based development environment. The UI is the most important part of it. So how does it work as a UI development suite? Or, how does it actually turn our code into interfaces that are rendered on the device’s square screen?

Three trees – View creation

Widgets and Element

Imagine if your company’s UI gave you an interface to complete it. How would you start? Was this a stateless or stateful page? If static, subclass a StatelessWidget and override its build() method, where we simply construct our page (Widget object) and return it outside the method. If you are dynamic, you need to customize the StatefulWidget and State subclasses, which we’ll look at in more detail here.

The build() method uses the graphics code we’ve been working on to create real, visible shapes, such as buttons, text, etc. How does flutter do this without anyone knowing about it?

Let’s start with the StatelessWidget’s build() method to see where the widgets it creates go and what they do.

/// StatelessElement  
@override
Widget build() => widget.build(this);
Copy the code
/// ComponentElement (StatelessElement parent)
@override
void performRebuild() {
    // ...
    Widget built;
    try {
        // ...
        built = build();
        // ...
    } catch (e, stack) {
        _debugDoingBuild = false;
        built = ErrorWidget.builder(
            _debugReportException(
                ErrorDescription('building $this'),
                e,
                stack,
                informationCollector: () sync* {
                    yield DiagnosticsDebugCreator(DebugCreator(this)); },),); }finally {
        // ...
    }
    try {
        _child = updateChild(_child, built, slot);
        assert(_child ! =null);
    } catch (e, stack) {
        built = ErrorWidget.builder(
            _debugReportException(
                ErrorDescription('building $this'),
                e,
                stack,
                informationCollector: () sync* {
                    yield DiagnosticsDebugCreator(DebugCreator(this)); },),); _child = updateChild(null, built, slot);
    }

    // ...
}
Copy the code

The widget we created is passed as a parameter to the updateChild() method.

/// Element
@protected
Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
    if (newWidget == null) {
        if(child ! =null)
            deactivateChild(child);
        return null;
    }
    Element newChild;
    if(child ! =null) {
        bool hasSameSuperclass = true;
        // ...
        // The types of widgets and elements (Stateless or Stateful, etc.) did not change and widgets did not change
        if (hasSameSuperclass && child.widget == newWidget) {
            if(child.slot ! = newSlot) updateSlotForChild(child, newSlot); newChild = child; }else if (hasSameSuperclass && Widget.canUpdate(child.widget, newWidget)) {
            /// The types of widgets and elements (Stateless or Stateful, etc.) did not change
            /// And the new and old widgets have the same runtime type and key (the contents of the canUpdate() method)
            if(child.slot ! = newSlot) updateSlotForChild(child, newSlot); child.update(newWidget);assert(child.widget == newWidget);
            assert(() {
                child.owner._debugElementWasRebuilt(child);
                return true; } ()); newChild = child; }else {
            /// For newly created widgets, you need to mount them here because they have not been mounted before
            deactivateChild(child);
            assert(child._parent == null); newChild = inflateWidget(newWidget, newSlot); }}else {
        newChild = inflateWidget(newWidget, newSlot);
    }

    / /...

    return newChild;
}
Copy the code

I’ve added some comments to the method above, so what I’m going to do is mount the widget we created into the view tree of the flutter and see what happens:

/// Element
@protected
Element inflateWidget(Widget newWidget, dynamic 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);
            returnupdatedChild; }}/// NewWidget is the StatelessWidget subclass that we created, passed in as an argument,
    /// Here we create an Element class with the newWidget.
    final Element newChild = newWidget.createElement();
    assert(() {
        _debugCheckForCycles(newChild);
        return true; } ());/// This step mounts the newly created Element to the Element tree
    newChild.mount(this, newSlot);
    assert(newChild._debugLifecycleState == _ElementLifecycle.active);
    return newChild;
}
Copy the code

The mount() method is to mount the created element to the element tree.

/// Element
void mount(Element parent, dynamic newSlot) {
    // ..._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;
    final Key key = widget.key;
    if (key is GlobalKey) {
        key._register(this);
    }
    _updateInheritance();
    // ...
}
Copy the code

The new element is linked to the end of the tree by a chain-like assignment. The _child variable of the parent node does not point to the new element. This is a one-way chain. If we go through the above method backwards, the newly created Element is returned. What is it used for? All the way up to the performRebuild() method for ComponentElement, where the element returned is assigned to the _Child variable of the parent node, the bidirectional linkage is complete.

One more variable to keep an eye on in advance is this “slot”, which seems to have followed us through methods so far, but doesn’t seem to have any real effect. It will be present in almost all of the methods we’ll focus on later, but since it doesn’t work for now, let’s just remember that there is such a thing, and it’s assigned to Element’s _slot member, and we’ll talk about it later when it needs to.

Now that we have a complete Element tree, how does that tree render to the screen? Let’s leave it for a moment and continue with the “tree planting” process for StatefulWidget.

StatefulWidget differs from StatelessWidget in that the StatefulWidget’s build() method is in State, The StatefulWidget creates the State with the createState() method. Let’s look at the invocation relationship:

As you can see, StatefulElement holds State. Now that you know that, look at the build() method for State:

/// StatefulElement
@override
Widget build() => _state.build(this);
Copy the code

StatefulElement, like StatelessElement, is a subclass of ComponentElement. So the same process that we saw above for ComponentElement’s performRebuild() method calling build() also applies to it, and the following process is the same as the StatelessWidget.

At this point, we’ve taken the mystery out of two trees — the Widget tree and the Element tree — and so far, they’re one to one. There is an instance of Widget, _widget, in the Element class member variable, which is assigned at Element creation and update() time, thus supporting the one-to-one correspondence. So what is the third tree?

RenderObject

We open the Widget class source code in Android Studio and go to Hierarchy (you can double click Shift or press Ctrl + Shift + A to search, or you can use your own shortcut). We see the following direct subclasses of widgets, and we notice that in addition to the StatelessWidget and StatefulWidget we’ve just touched on, there are several other unfamiliar subclasses.

PreferredSizeWidget and ProxyWidget are simple encapsulation of a Widget’s requirements, each extending a property, so they can be considered equivalent to widgets to a certain extent. There is also a RenderObjectWidget. What does this class do? Let’s have a look at its source code:

/// RenderObjectWidgets provide the configuration for [RenderObjectElement]s,
/// which wrap [RenderObject]s, which provide the actual rendering of the
/// application.
abstract class RenderObjectWidget extends Widget {
  /// Abstract const constructor. This constructor enables subclasses to provide
  /// const constructors so that they can be used in const expressions.
  const RenderObjectWidget({ Key key }) : super(key: key);

  /// RenderObjectWidgets always inflate to a [RenderObjectElement] subclass.
  @override
  @factory
  RenderObjectElement createElement();

  /// Creates an instance of the [RenderObject] class that this
  /// [RenderObjectWidget] represents, using the configuration described by this
  /// [RenderObjectWidget].
  ///
  /// This method should not do anything with the children of the render object.
  /// That should instead be handled by the method that overrides
  /// [RenderObjectElement.mount] in the object rendered by this object's
  /// [createElement] method. See, for example,
  /// [SingleChildRenderObjectElement.mount].
  @protected
  @factory
  RenderObject createRenderObject(BuildContext context);

  /// Copies the configuration described by this [RenderObjectWidget] to the
  /// given [RenderObject], which will be of the same type as returned by this
  /// object's [createRenderObject].
  ///
  /// This method should not do anything to update the children of the render
  /// object. That should instead be handled by the method that overrides
  /// [RenderObjectElement.update] in the object rendered by this object's
  /// [createElement] method. See, for example,
  /// [SingleChildRenderObjectElement.update].
  @protected
  void updateRenderObject(BuildContext context, covariant RenderObject renderObject) { }

  /// A render object previously associated with this widget has been removed
  /// from the tree. The given [RenderObject] will be of the same type as
  /// returned by this object's [createRenderObject].
  @protected
  void didUnmountRenderObject(covariant RenderObject renderObject) { }
}
Copy the code

As you can see, it has three more methods than the StatelessWidget and StatefulWidget, These are createRenderObject(), updateRenderObject(), and didUnmountRenderObject(). Let’s look at the creation process first.

The mount() method of RenderObjectElement calls the createRenderObject() method:

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

The created RenderObject is assigned to _renderObject and stored, followed by attachRenderObject() :

/// RenderObjectElement
@override
void attachRenderObject(dynamic newSlot) {
    assert(_ancestorRenderObjectElement == null); _slot = newSlot; _ancestorRenderObjectElement = _findAncestorRenderObjectElement(); _ancestorRenderObjectElement? .insertRenderObjectChild(renderObject, newSlot);final ParentDataElement<ParentData> parentDataElement = _findAncestorParentDataElement();
    if(parentDataElement ! =null)
        _updateParentData(parentDataElement.widget);
}
Copy the code

_findAncestorRenderObjectElement () method is used up for the first RenderObjectElement object instance:

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

The RenderObject does not correspond to the Element tree, nor to the Widget tree. The statelessWidgets and StatefulWidgets we create are more of a container. They are not actually visible view elements, but rather a set of permutations of visible view elements, specifying their positions, sizes, etc., so they do not need to be rendered. Widgets that inherit from RenderObjectWidget need to be rendered. There are corresponding nodes in the RenderObject tree.

Ok, back to business, then call the insertRenderObjectChild() method of the first RenderObjectElement ancestor, which is now implemented as a rewrite, Then we look at two more commonly used RenderObjectElement subclasses, and the realization of the MultiChildRenderObjectElement SingleChildRenderObjectElement. The first is SingleChildRenderObjectElement:

/// SingleChildRenderObjectElement
@override
void insertRenderObjectChild(RenderObject child, dynamic slot) {
    final RenderObjectWithChildMixin<RenderObject> renderObject = this.renderObject as RenderObjectWithChildMixin<RenderObject>;
    assert(slot == null);
    assert(renderObject.debugValidateChild(child));
    renderObject.child = child;
    assert(renderObject == this.renderObject);
}
Copy the code

Note that “this” is the ancestor of the first RenderObjectElement type, so this.renderObject is also the parent of the renderObject, and child is the renderObject instance we created. Implement the same link as Element above by assigning a value to the Child member of the RenderObject.

While in MultiChildRenderObjectElement method, through the mixin ContainerRenderObjectMixin insert:

/// MultiChildRenderObjectElement
@override
void insertRenderObjectChild(RenderObject child, IndexedSlot<Element> slot) {
    final ContainerRenderObjectMixin<RenderObject, ContainerParentDataMixin<RenderObject>> renderObject =
        this.renderObject as ContainerRenderObjectMixin<RenderObject, ContainerParentDataMixin<RenderObject>>;
    assert(renderObject.debugValidateChild(child)); renderObject.insert(child, after: slot? .value? .renderObject);assert(renderObject == this.renderObject);
}
 
/// ContainerRenderObjectMixin is actually RenderObject
mixin ContainerRenderObjectMixin<ChildType extends RenderObject, ParentDataType extends ContainerParentDataMixin<ChildType>> on RenderObject {
	/// .
}
Copy the code

The key code here is renderObject.insert(child, after: slot? .value? .renderObject), but before we can understand the implementation of insert(), we need to clarify the second argument passed after: slot? .value? RenderObject is a god. Slot we also cautioned readers above to keep a little bit of an impression of it, because it really followed us all the way. Along the way, it’s been mysterious and low-key, but now, it’s not low-key, because it’s finally going to work, and as we study it, it’s going to stop being mysterious. Let’s start with the Element class comment:

/// Information set by parent to define where this child fits in its parent's
/// child list.
///
/// Subclasses of Element that only have one child should use null for
/// the slot for that child.
dynamic get slot => _slot;
dynamic _slot;
Copy the code

Oh, the child’s parent node is used to determine the location of the child. The emphasis on “child” is to indicate that it is stored in the child’s object space to find the appropriate location of the child relative to the parent node. So it sounds like the parent node should have more than one child, otherwise there is no such thing as an appropriate or inappropriate location. Indeed, this variable is generally null for single-child nodes, but becomes meaningful for multi-child nodes.

Back to the MultiChildRenderObjectElement mount () method:

@override
void mount(Element parent, dynamic newSlot) {
    super.mount(parent, newSlot);
    _children = List<Element>(widget.children.length);
    Element previousChild;
    for (int i = 0; i < _children.length; i += 1) {
        final Element newChild = inflateWidget(widget.children[i], IndexedSlot<Element>(i, previousChild)); _children[i] = newChild; previousChild = newChild; }}@protected
Element inflateWidget(Widget newWidget, dynamic newSlot) {
    // ...
    newChild.mount(this, newSlot);
    // ...
    return newChild;
}

@immutable
class IndexedSlot<T> {
  /// Creates an [IndexedSlot] with the provided [index] and slot [value].
  const IndexedSlot(this.index, this.value);

  /// Information to define where the child occupying this slot fits in its
  /// parent's child list.
  final T value;

  /// The index of this slot in the parent's child list.
  final int index;
 
  /// .
}
Copy the code

Slot is IndexedSlot

(I, previousChild), whose value points to the previous sibling. Renderobject.insert () = renderObject.insert(); renderObject.insert() = renderObject.insert();

/// ContainerRenderObjectMixin
ChildType? _firstChild;
ChildType? _lastChild;
/// [child] Newly created RenderObject class,
/// [after] The newly created RenderObject needs to be inserted after the parent node
void _insertIntoChildList(ChildType child, { ChildType? after }) {
    final ParentDataType childParentData = child.parentData as ParentDataType;
    // ...
    _childCount += 1;
    assert(_childCount > 0);
    if (after == null) {
        // insert at the start (_firstChild)
        childParentData.nextSibling = _firstChild;
        if(_firstChild ! =null) {
            finalParentDataType _firstChildParentData = _firstChild! .parentDataasParentDataType; _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
            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
            // set up links from child to siblings
            childParentData.nextSibling = afterParentData.nextSibling;
            childParentData.previousSibling = after;
            // set up links from siblings to child
            finalParentDataType childPreviousSiblingParentData = childParentData.previousSibling! .parentDataas ParentDataType;
            finalParentDataType childNextSiblingParentData = childParentData.nextSibling! .parentDataas ParentDataType;
            childPreviousSiblingParentData.nextSibling = child;
            childNextSiblingParentData.previousSibling = child;
            assert(afterParentData.nextSibling == child); }}}Copy the code

The logic of the code here is clear, with the children and siblings clearly linked together, and the RenderObject tree grows out of this open leaf.

Thriving – View updates

We explored the process of creating nodes for three trees and inserting them into the tree, and it was this mechanism that allowed our three trees to grow and grow, and we were able to create our own giant trees with our own code. But to make sure the tree thrives, we trim the dead branches, leaving space and nutrients for the new shoots. So how do we update those “branches” and “leaves” that are outdated or in a state that is no longer correct?

We usually use the setState() method in State to update StatefulWidget, so let’s start with this method and see what Flutter does after we manually invoke the method.

/// State
@protected
void setState(VoidCallback fn) {
    assert(fn ! =null);
    // ...
    final dynamic result = fn() as dynamic;
    // ...
    _element.markNeedsBuild();
}
Copy the code

The callback function passed in the setState() method is executed first, and the StatefulElement corresponding to the StatefulWidget is marked as needing to be rebuilt.

/// Element
void markNeedsBuild() {
    // ...
    if (dirty)
        return;
    _dirty = true;
    owner.scheduleBuildFor(this);
}
Copy the code

Next, change the _dirty attribute to true, which identifies whether the element needs to be rebuilt, and call the BuildOwner scheduleBuildFor() method. Here’s a quick overview of the BuildOwner class, but as usual, look at the official comment:

Manager class for the widgets framework.

This class tracks which widgets need rebuilding, and handles other tasks that apply to widget trees as a whole, such as managing the inactive element list for the tree and triggering the “reassemble” command when necessary during hot reload when debugging.

The main build owner is typically owned by the [WidgetsBinding], and is driven from the operating system along with the rest of the build/layout/paint pipeline.

Additional build owners can be built to manage off-screen widget trees.

To assign a build owner to a tree, use the [RootRenderObjectElement.assignOwner] method on the root element of the widget tree.

As you can see, this class is used to manage the view framework’s functions such as rebuild, Inactive state, and so on. It is created and assigned to the root node in WidgetBinding, and then passed down to each node according to the view tree, so there is only one instance of it in the entire view tree. More relevant knowledge points you can study yourself.

Back to the scheduleBuildFor() method:

/// BuildOwner

/// Adds an element to the dirty elements list so that it will be rebuilt
/// when [WidgetsBinding.drawFrame] calls [buildScope].
void scheduleBuildFor(Element element) {
    // ...
    if (element._inDirtyList) {
        // ...
        _dirtyElementsNeedsResorting = true;
        return;
    }
    // Refresh dirty data when not scheduled
    // If it is currently between two refresh frames, this method will eventually call the [window.scheduleFrame] method in preparation for the next frame
    if(! _scheduledFlushDirtyElements && onBuildScheduled ! =null) {
        _scheduledFlushDirtyElements = true;
        onBuildScheduled();
    }
    _dirtyElements.add(element);
    element._inDirtyList = true;
    // ...
}
Copy the code

After registering all nodes that need to be updated with _dirtyElements. Add (element), these dirty nodes are refreshed in order of the depth of the tree when the next Vsync signal is received for view refresh. This is all done in the widgetsBinding.drawFrame () method, but we won’t go into the details of this method for now. Let’s look at its calling method:

void buildScope(Element context, [ VoidCallback callback ]) {
    // ...
    Timeline.startSync('Build', arguments: timelineArgumentsIndicatingLandmarkEvent);
    try {
      _scheduledFlushDirtyElements = true;
      // ...
      _dirtyElements.sort(Element._sort);
      _dirtyElementsNeedsResorting = false;
      int dirtyCount = _dirtyElements.length;
      int index = 0;
      while (index < dirtyCount) {
        // ...
        try {
          _dirtyElements[index].rebuild();
        } catch (e, stack) {
          // ...
        }
        index += 1;
        if (dirtyCount < _dirtyElements.length || _dirtyElementsNeedsResorting) {
          _dirtyElements.sort(Element._sort);
          _dirtyElementsNeedsResorting = false;
          dirtyCount = _dirtyElements.length;
          while (index > 0 && _dirtyElements[index - 1].dirty) {
            // It is possible for previously dirty but inactive widgets to move right in the list.
            // We therefore have to move the index left in the list to account for this.
            // We don't know how many could have moved. However, we do know that the only possible
            // change to the list is that nodes that were previously to the left of the index have
            // now moved to be to the right of the right-most cleaned node, and we do know that
            // all the clean nodes were to the left of the index. So we move the index left
            // until just after the right-most clean node.
            index -= 1; }}}// ...
    } finally {
      for (final Element element in _dirtyElements) {
        assert(element._inDirtyList);
        element._inDirtyList = false;
      }
      _dirtyElements.clear();
      _scheduledFlushDirtyElements = false;
      _dirtyElementsNeedsResorting = null;
      Timeline.finishSync();
      // ...
    }
    // ...
  }
Copy the code

The previously registered dirty nodes are traversed, their rebuild() method is called in turn, and after executing the build() method, the view tree needs to be reordered if necessary to prevent changes to the view tree. At the end, all registered dirty nodes are cleared.

The rebuild() method calls the performRebuild() method, which brings us back to where we started, where we create the Widget by calling the Widget’s build() method, This is why the build() method is re-executed after we call setState() manually. Note, however, that in the updateChild() method, the widget tree is not remounted because the Element may already correspond to the widget. Instead, the widget node is updated.

RenderObjectElement only has a corresponding RenderObject. RenderObjectElement’s performRebuild() method will update the dirty renderObject. The update logic is implemented by RenderObjectWidget subclass:

@override
void performRebuild() {
    // ..
    widget.updateRenderObject(this, renderObject);
    // ..
    _dirty = false;
}
Copy the code

In this way, all three trees are covered with new trees, and it is by this mechanism that all three trees are able to cut off the bad branches and grow healthily.

Write last (skip the nonsense)

This article mainly studies the three trees of the Widget and the update mechanism of the three trees from common methods. Due to the limited space, there are still many problems that have not been discussed. For example, we left the question above: How does the RenderObject tree become a picture? More questions, we will continue to explore together in a length. All the content of the above article I am not very understand in advance, but also while writing while looking at the source grope, and self-knowledge skill is limited, so there are a lot of logic is not rigorous and omissions, that if the wrong place, welcome yazheng, help me this knock on the code of pupil progress, thank you.