Widgets, Elements, and RenderObjects

Widget

Widgets are descriptions of user pages that represent Element configuration information. A Flutter page is declared by a combination of widgets. Widgets themselves are immutable, as noted below:

@immutable
abstract class Widget extends DiagnosticableTree {/// . }
Copy the code

This means that all variables that it directly declares or inherits must be of final type. If you want to give a widget associated with a variable State, consider using StatefulWidget, it will create a through [StatefulWidget. CreateState] State object, and then, when it is converted into an element will be merged into a tree.

The subclass:

Statelesswidgets and StatefulWidgets are familiar for writing pages and components, but what about the other three?

  • RenderObjectWidget, as the name suggests, is a Widget, and it has an inextricable relationship to the actual RenderObject, RenderObject. It provides configuration information for RenderObjectElement, which wraps the RenderObject. The StatelessWidget and StatefulWidget that are written from the page will return the actual renderable Widget object, RenderObjectWidget, in the recursive build process. We’ll talk about it later
  • PreferredSizeWidget, a component that returns its own desired size if it is unrestricted during layout, for example, AppBar and TabBar
  • ProxyWidget, a proxy component, provides a child component rather than creating it itself, for example, InheritedWidget and ParentDataWidget

Element

The element tree, which is an instantiation of a Widget at a specific location, controls the Widget’s lifecycle, holds Widget instances and renderObject instances, and inherits from the same class as Widget, DiagnosticableTree. And implements the BuildContext class.

There are two basic types of elements:

  • ComponentElement, the host of other elements, does not itself contain the RenderObject, but element nodes held by it contain, StatelessElement and StatefulElement created in StatelessWidget and StatefulElement, respectively, inherit from ComponentElement
  • RenderObjectElement, an element that participates in the layout or drawing phase

RenderObject

The base class for each node in the render tree is RenderObject, which defines the abstract model for layout and drawing. Each RenderObject has a parent and a parentData in which the parent RenderObject can store specific data about the child, such as the location of the child.

  • RenderObject only implements the basic layout and rendering without a specific layout rendering model, which is equivalent to ViewGroup. Its subclass RenderBox uses a Cartesian coordinate system, and some of its subclasses are real nodes in the rendering tree. In most cases, when we want to customize a RenderObject, directly inheriting RenderObject is overkill. It’s better to inherit RenderBox, unless you don’t want to use cartesian coordinates.
  • RenderView, which is usually the root node of the Flutter rendering tree, can be understood as a DecorView. It has only one child node and must be of type RenderBox.

Corresponding relations between

Build an Element from a Widget

Look at this simple code snippet that shows the widget tree structure

Container(
  color: Colors.blue,
  child: Row(
    children: [
      Image.network('https://www.example.com/1.png'),
      const Text('A'),,),);Copy the code

When a Flutter renders the Container to the page, it calls its build() method, which returns a widget subtree containing its child tree Row and children, as well as some other tree nodes. Look at its build() function:

class Container extends StatelessWidget {
  /// Create a component that combines common drawing, positioning, and sizing controls
	Container({
    Key? key,
    this.alignment,
    this.padding,
    this.color,
    this.decoration,
    this.foregroundDecoration,
    double? width,
    double? height,
    BoxConstraints? constraints,
    this.margin,
    this.transform,
    this.transformAlignment,
    this.child,
    this.clipBehavior = Clip.none,
  }) : // ...
  
  @override
  Widget build(BuildContext context) {
    Widget? current = child;
		// ...
    if(alignment ! =null) current = Align(alignment: alignment! , child: current);// ...
    if(effectivePadding ! =null)
      current = Padding(padding: effectivePadding, child: current);

    if(color ! =null) current = ColoredBox(color: color! , child: current);// ...
    if(decoration ! =null) current = DecoratedBox(decoration: decoration! , child: current);return current!;
  }
}
Copy the code

As you can see, some of the attributes of the Container represent the insertion of a new node widget that controls those attributes, so it is a wrapper in itself that combines a lot of widgets for us and reduces the development effort. We set the color property, which inserts a ColoredBox node to show its color.

Correspondingly, Image and Text may also insert child nodes such as RawImage and RichText during build, so the widget tree may have a deeper hierarchy than the code suggests

During the construction phase, Flutter transforms the widgets described above into the corresponding Element tree, one to one. Each element in the hierarchy of the tree represents an instance of the widget at a specific location.

In this case, the one-to-one mapping refers to the converted widgets in the framework layer, rather than the mapping between widgets written by users in the code layer and Elements. For example, a Container is converted into multiple sub-widgets that correspond to multiple Element nodes after setting its attributes.

Element implements BuildContext. The Element of any widget can be accessed by the BuildContext parameter passed to the build() method, which is the widget’s handle to the tree operation. For example, you can call Theme.of(context) to find the most recent Theme in the widget tree and return it if the widget defines a separate Theme, or the app Theme if it doesn’t

/// An [Element] that uses a [StatelessWidget] as its configuration.
class StatelessElement extends ComponentElement {
  /// Creates an element that uses the given widget as its configuration.
  StatelessElement(StatelessWidget widget) : super(widget);

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

  @override
  Widget build() => widget.build(this);

  @override
  void update(StatelessWidget newWidget) {
    super.update(newWidget);
    assert(widget == newWidget);
    _dirty = true; rebuild(); }}Copy the code

As you can see, the StatelessElement element calls the Build method at build time, which calls the StatelessWidget’s Build method, passing BuildContext as this.

Because widgets are immutable, including parent/child relationships between nodes, any modification to the widget tree (for example, Text(‘A’) to Text(‘B’)) causes A new set of widget objects to be reconstructed. This does not mean that the underlying layer has to be rebuilt. Element Tree may be persistent when the interface is refreshed and therefore plays a key role in performance because Flutter caches the underlying representation, making it behave as if the upper layer of widgets were discarded completely. It is possible to rebuild only a portion of the Element Tree by iterating through the widgets’ modifications.

Element to the RenderObject

It is rare to draw a single widget, so an important part of any UI framework is to be able to efficiently lay out a hierarchical widget, determine its size, location, and then draw it onto the screen.

The base type of each node in the rendering tree is RenderObject. At the construction stage, Flutter only generates RenderObjectElement objects from element Tree into renderable objects. Different Render objects Render different types. RenderParagraph Renders the text, RenderImage renders the image

The rendering objects of most Flutter widgets are inherited from RenderBox, which uses a Cartesian coordinate system in 2D space. It provides a box constraint model that limits the minimum and maximum widths and heights of widgets.

During layout, a Flutter traverses the render tree depth-first, passing constraints to the child to determine its size, and then passing the result to the parent’s size variable.

/// Subclasses should not override the [Layout] method directly, but should override the [performResize] and/or [performLayout], [Layout] methods
/// The agent works on [performResize] and [performLayout]
/// Parent's [performLayout] method should unconditionally call all of its children's [Layout]
void layout(Constraints constraints, { bool parentUsesSize = false{})/// .
    try {
      performLayout();
      markNeedsSemanticsUpdate();
      
    } catch (e, stack) {
      _debugReportException('performLayout', e, stack);
    }
    /// .
    _needsLayout = false;
    markNeedsPaint();
 }

/// Empty implementation, overridden by subclasses
  @protected
 	void performLayout();
Copy the code

For example, look at the performLayout method for RenderPadding:

@override
  void performLayout() {
    /// The first step is to get to constraints
    final BoxConstraints constraints = this.constraints;
    // ...
    /// The second step is to calculate its own internal constraints from the parent's constraints
    finalBoxConstraints innerConstraints = constraints.deflate(_resolvedPadding!) ;/// Step 3, continue down through the Layoutchild! .layout(innerConstraints, parentUsesSize:true);
    finalBoxParentData childParentData = child! .parentData!asBoxParentData; childParentData.offset = Offset(_resolvedPadding! .left, _resolvedPadding! .top);/// Fourth, generate size based on constraintssize = constraints.constrain(Size( _resolvedPadding! .left + child! .size.width + _resolvedPadding! .right, _resolvedPadding! .top + child! .size.height + _resolvedPadding! .bottom, )); }Copy the code

This completes the tree’s depth traversal

The box constraint model is a very powerful way of laying out objects in order n time.

The root node of all RenderObjects is the RenderView, which represents the output of the entire render tree. The compositeFrame() method in the RenderView object is called when the platform needs to render a new frame (for example, when a vsync signal is triggered, or when the extract/upload of texture is completed), which creates a SceneBuilder that triggers an update of the screen. When the update is complete, the RenderView passes the compressed scene to the window.render () method in the Dart: UI package, which controls the GPU to render it.

Is there a one-to-one correspondence

As you can easily see from the picture above, no.

type Widget Element RenderObject instructions
combination StatelessWidget ComponentElement NA Combined nodes that do not correspond to renderObjects
StatefulWidget NA
The agent type ProxyWidget NA Proxy component, data transfer
display RenderObjectWidget RenderObjectElement RenderObject Actual render object

The table lists only common widgets and mappings, not all of them

So it’s contextual to say that widgets are one-to-one with Elements and RenderObjects, which is fine in the case of the display line, but not accurate at the global level.

Set up process

The above is a cursory look at the transformation process of the three trees, so at the code level, how are they connected through method calls? It can be divided into two main processes:

The root view attachRootWidget

Initialize the Widget tree Element and RenderObject tree root node, respectively is RenderObjectToWidgetAdapter, RenderObjectToWidgetElement, RenderView.

Then WidgetsBinding. AttachRootWidget method, runApp into rootWidget added to the widget tree roots RenderObjectToWidgetAdapter instance on the child, Call its attachToRenderTree, attach element to the RenderTree, and call Element’s mount method.

/// Takes a widget and attaches it to the [renderViewElement], creating it if
  /// necessary.
  /// This is called by [runApp] to configure the widget tree.
  ///  * [RenderObjectToWidgetAdapter.attachToRenderTree], which inflates a
  ///    widget and attaches it to the render tree.
  void attachRootWidget(Widget rootWidget) {
    final bool isBootstrapFrame = renderViewElement == null;
    _readyToProduceFrames = true;
    _renderViewElement = RenderObjectToWidgetAdapter<RenderBox>(
      container: renderView,
      debugShortDescription: '[root]', child: rootWidget, ).attachToRenderTree(buildOwner! , renderViewElementasRenderObjectToWidgetElement<RenderBox>?) ;if (isBootstrapFrame) {
      SchedulerBinding.instance!.ensureVisualUpdate();
    }
  }
Copy the code

RenderView is the root node of the RenderObject Tree, initialized in the RendererBinding class

/// The glue between the render tree and the Flutter engine.
/// Glue between Render Tree and Flutter Engine
mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureBinding, SemanticsBinding, HitTestable {
	 @override
  void initInstances() {
    super.initInstances();
    /// .
    initRenderView();
   /// .
  }
  
  void initRenderView() {
		/// .
    renderView = RenderView(configuration: createViewConfiguration(), window: window); renderView.prepareInitialFrame(); }}Copy the code

AttachToRenderTree method

/// Used by [runApp] to bootstrap applications.
/// Used by runApp to boot the program
class RenderObjectToWidgetAdapter<T extends RenderObject> extends RenderObjectWidget {
	/// Used by [runApp] to bootstrap applications.
  RenderObjectToWidgetElement<T> attachToRenderTree(BuildOwner owner, [ 	         RenderObjectToWidgetElement<T>? element ]) {
    if (element == null) {
      owner.lockState(() {
        element = createElement();
        assert(element ! =null); element! .assignOwner(owner); }); owner.buildScope(element! , () { element! .mount(null.null);
      });
    } else {
      element._newWidget = this;
      element.markNeedsBuild();
    }
    returnelement! ; } RenderObjectToWidgetElement<T> createElement() => RenderObjectToWidgetElement<T>(this);

}
Copy the code

This element is empty, so created RenderObjectToWidgetElement instance, then the mount.

The view of attachToRenderTree

RenderObjectElement (RenderObjectElement, RenderObjectElement) Call attachRenderObject to mount to the RenderObject tree. _rebuild→updateChild→inflateWidget→newWidget.createElement→newChild.mount(this, newSlot)

The key point is, newChild. Mount Element method will be called the subtypes of mainly two SingleChildRenderObjectElement and MultiChildRenderObjectElement, name is obvious, Element for one or more children. Mount method is as follows

class SingleChildRenderObjectElement extends RenderObjectElement {
	@override
  void mount(Element? parent, Object? newSlot) {
    super.mount(parent, newSlot);
    _child = updateChild(_child, widget.child, null); }}class MultiChildRenderObjectElement extends RenderObjectElement {
	@override
  void mount(Element? parent, Object? newSlot) {
    super.mount(parent, newSlot);
    final List<Element> children = List<Element>.filled(widget.children.length, _NullElement.instance, growable: false);
    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; } _children = children; }}Copy the code

So they do two things:

  • Call super.mount() to mount Element to Element Tree, createRenderObject, attachRenderObject, _renderObject to RenderObject tree
  • UpdateChild, passing in Widget. child, continues the widget tree conversion at the next level, where slot is passed as null and IndexedSlot objects, respectively

If the Element node is of type ComponentElement, the mount method is as follows

abstract class ComponentElement extends Element {
	@override
  void mount(Element? parent, Object? newSlot) {
    super.mount(parent, newSlot);
    /// .
    _firstBuild();
    assert(_child ! =null);
  }
  
  /// And eventually it will go to performRebuild
  @override
  void performRebuild() {
    Widget? built;
    try {
      /// The build() function that we often rewrite in code is right here
      built = build();
    } catch (e, stack) {
      /// Build the error page ErrorWidget and we see the error red page
      built = ErrorWidget.builder(
        _debugReportException(
          ErrorDescription('building $this'),
          e,
          stack,
          informationCollector: () sync* {
            yield DiagnosticsDebugCreator(DebugCreator(this)); },),); }/// Update the widget and continue the loop
    _child = updateChild(_child, built, slot);
     
  }
  /// In StatelessWidget/StafulWidget rewriting method
  @protected
  Widget build();
}
Copy the code

Slot object

What does the slot object that updateChild passes in do? To mark where the RenderObject is mounted on the RenderObject tree.

First, each Element will eventually wrap a RenderObject, which will eventually be attached to the RenderObject Tree, either by itself or by its descendants. So, when the direct child Element does not contain RenderObject StatelessElement/StatefulElement, for example, It marks which node on the RenderObject tree to mount the next RenderObject. So, the slot value passed in the updateChild method of their parent ComponentElement class is the location to mount. An Element node like this passes slot all the way down to the RenderObjectElement node.

So when does this value get initialized and passed down? SingleChildRenderObjectElement passed down is null, it does not need to slot, see attachRenderObject method

@override
  void attachRenderObject(Object? newSlot) {
    assert(_ancestorRenderObjectElement == null);
    _slot = newSlot;
    /// Find the ancestor node of the RenderObjectElement object
    _ancestorRenderObjectElement = _findAncestorRenderObjectElement();
    /// According to the newSlot slot, insert the renderObject into the render tree_ancestorRenderObjectElement? .insertRenderObjectChild(renderObject, newSlot);final ParentDataElement<ParentData>? parentDataElement = _findAncestorParentDataElement();
    if(parentDataElement ! =null)
      _updateParentData(parentDataElement.widget);
  }

RenderObjectElement? _findAncestorRenderObjectElement() {
    Element? ancestor = _parent;
  /// We loop up to the first object in RenderObjectElement to find the parent of the RenderObject
    while(ancestor ! =null && ancestor is! RenderObjectElement)
      ancestor = ancestor._parent;
    return ancestor asRenderObjectElement? ; }Copy the code

So a single child SingleChildRenderObjectElement don’t need a slot, because we find ancestor hardpoints. And MultiChildRenderObjectElement, with more than one child to find the same ancestor nodes, so there will be a slot will be brother nodes in sequence, generate IndexedSlot < Element? > (I, previousChild) slot, which is the original slot passed down, so the slot is from MultiChildRenderObjectElement node differentiation

This excludes the _rootChildSlot node where the render tree was first created

This completes a well-organized tree structure between the parent and sibling elements of the Element Tree and the RenderObject Tree. RenderObjectElement plays a central role in the process, controlling widget updates down and RenderObject generation to the correct nodes of the Render Tree.

conclusion

This is the first chapter of the understanding of three trees, focusing on the analysis of the establishment process of three trees. In the next chapter, we will continue to analyze the refreshing process of three trees, as well as the reason for the design of three trees, as well as the understanding of the concept of three trees, what guidance or attention to our development.

The article inevitably has the individual understanding, has the deviation place, asks everybody to criticize corrects, thanks!

reference

Flutter. Dev/docs/resour…

www.yuque.com/xytech/flut…