Render Tree

What are the render object building blocks?

  • RenderObject provides the basic infrastructure for modeling hierarchical visual elements in the UI and includes a generalized protocol for performing layout, painting, and compositing. This protocol is unopinionated, deferring to subclasses to determine the inputs and outputs of layout, how hit testing is performed (though all render objects are HitTestTargets), and even how the render object hierarchy is modeled.

  • Constraints represent the immutable inputs to layout. They must indicate whether they offer no choice (Constraints.isTight), are the canonical representation (Constraints.isNormalized), and provide “==” and hashCode implementations.

  • ParentData is opaque data stored in a child RenderObject by its parent. The only requirement is that ParentData instances implement a ParentData.detach method to react to their render object being removed from the tree.

How is the render object lifecycle managed?

  • The RendererBinding establishes a PipelineOwner as part of RendererBinding.initInstances (i.e., the binding’s “constructor”). The PipelineOwner is analogous to the BuildOwner, tracking which render objects need compositing bits, layout, painting, and semantics updates. Objects mark themselves “dirty” by adding themselves to lists, e.g., PipelineOwner._nodesNeedingLayout, PipeplineOwner._nodesNeedingPaint, etc. All dirty objects are later cleaned by the corresponding “flush” method: PipelineOwner.flushLayout, PipelineOwner.flushPaint, etc. These methods form the majority of the rendering pipeline and are invoked every frame via RendererBinding.drawFrame.

  • Whenever an object marks itself dirty, it will generally also invoke PipelineOwner.requestVisualUpdate to schedule a frame and update the UI. This calls the PipelineOwner.onNeedVisualUpdate handler which schedules a frame via RendererBinding.ensureVisualUpdate and SchedulerBinding.scheduleFrame. The rendering pipeline (as facilitated by RendererBinding.drawFrame or WidgetsBinding.drawFrame) will invoke the PipelineOwner’s various flush methods to drive each stage.

  • When the render object is attached to its PipelineOwner, it will mark the render object as dirty in all necessary dimensions (e.g., RenderObject.markNeedsLayout, RenderObject.markNeedsPaint). Generally, all are invoked since the internal flags that are consulted are initialized to true (e.g., RenderObject._needsLayout, RenderObject._needsPaint).

How is the render tree structured?

  • The render tree consists of AbstractNode instances (RenderObject is a subclass). When a child is added or removed, RenderObject.adoptChild and RenderObject.dropChild must be called, respectively. When altering the depth of a child, RenderObject.redepthChild must be called to keep RenderObject.depth in sync.

  • Every node also needs to use RenderObject.attach and RenderObject.detach to set and clear the pipeline owner (RenderObject.owner), respectively. Every RenderObject has a parent (RenderObject.parent) and an attached state (RenderObject.attached).

  • Parent data is a ParentData subclass optionally associated with a render object. By convention, the child is not to access this data, though a protocol is free to alter this rule (but shouldn’t). When adding a child, the parent data is initialized by calling RenderObject.setupParentData. The parent data may then be mutated in, e.g., RenderObject.performLayout.

  • All RenderObjects implement the visitor pattern via RenderObject.RenderObject.visitChildren which invokes a RenderObjectVisitor once per child.

What is necessary when altering the child model?

  • When adding or removing a child, call RenderObject.adoptChild and RenderObject.dropChild respectively.

  • The parent’s RenderObject.attach and RenderObject.detach methods must call their counterpart on each child.

  • The parent’s RenderObject.redepthChildren and RenderObject.visitChildren methods must recursively call RenderObject.redepthChild and the visitor argument on each child, respectively.

What are the render tree building blocks?

  • RenderObjectWithChildMixin allocates storage for a single child within the object instance itself (RenderObjectWithChildMixin.child).

  • ContainerRenderObjectMixin uses each child’s parent data (which must implement ContainerParentDataMixin) to build a doubly linked list via ContainerParentDataMixin.nextSibling and ContainerParentDataMixin.previousSibling.

    • A variety of container-type methods are included: ContainerRenderObjectMixin.insert, ContainerRenderObjectMixin.add, ContainerRenderObjectMixin.move, ContainerRenderObjectMixin.remove, etc.

    • These adopt, drop, attach, and detach children appropriately. Additionally, when the child list changes, RenderObject.markNeedsLayout is invoked (as this may alter layout).

How do render objects manage layout?

  • When a render object has been marked as dirty via RenderObject.markNeedsLayout, RenderObject.layout will be invoked with constraints as input and a size as output. Both the constraints and the size are implementation-dependent; the constraints must implement Constraints whereas the size is entirely arbitrary.

  • If a parent depends on the child’s geometry, it must pass the parentUsesSize argument to layout. The implementation of RenderObject.markNeedsLayout / RenderObject.sizedByParent will need to call RenderObject.markParentNeedsLayout / RenderObject.markParentNeedsLayoutForSizedByParentChange, respectively.

  • RenderObjects that solely determine their sizing using the input constraints must set RenderObject.sizedByParent to true and perform all layout in RenderObject.performResize.

  • Layout cannot depend on position (typically stored using parent data). The position, if applicable, is solely determined by the parent. However, some RenderObject subtypes may utilize additional out-of-band information when performing layout. If this information changes, and the parent used it during the last cycle, RenderObject.markParentNeedsLayout must be invoked.

How do render objects manage hit testing?

  • RenderView’s child is a RenderBox, which must therefore define RenderBox.hitTest. To add a custom RenderObject to the render tree, the top level RenderView must be replaced (which would be a massive undertaking) or a RenderBox adapter added to the tree. It is up to the implementer to determine how RenderBox.hitTest is adapted to a custom RenderObject subclass; indeed, that render object can implement any manner of hitTest-like method of its choosing. Note that all RenderObjects are HitTestTargets and therefore will receive pointer events via HitTestTarget.pointerEvent once registered via HitTestEntry.

How are render objects composited into layers?

  • Render objects track a “needsCompositing” bit (as well as an “alwaysNeedsCompositing” bit and a “isRepaintBoundary” bit, which are consulted when updating “needsCompositing”). This bit indicates to the framework that a render object will paint into its own layer.

  • This bit is not set directly. Instead, it must be marked dirty whenever it might possibly change (e.g., when adding or removing a child). RenderObject.markNeedsCompositingBitsUpdate walks up the render tree, marking objects as dirty until it reaches a previously dirtied render object or a repaint boundary.

  • Later, just before painting, PipelineOwner.flushCompositingBits visits all dirty render objects, updating the “needsCompositing” bit by walking down the tree, looking for any descendent that needs compositing. This walk stops upon reaching a repaint boundary or an object that always needs compositing. If any descendent node needs compositing, all nodes along the walk need compositing, too.

  • It is important to create small “repaint sandwiches” to avoid introducing too many layers. [?]

  • Composited render objects are associated with an optional ContainerLayer. For render objects that are repaint boundaries, RenderObject.layer will be OffsetLayer. In either case, by retaining a reference to a previously used layer, Flutter is able to better utilize retained rendering -- i.e., recycle or selectively update previously rasterized bitmaps. This is possible because Layers preserve a reference to any underlying EngineLayer, and SceneBuilder accepts an “oldLayer” argument when building a new scene.

How do render objects handle transformations?

  • Visual transforms are implemented in RenderObject.paint. The same transform is applied logically (i.e., when hit testing, computing semantics, mapping coordinates, etc) via RenderObject.applyPaintTransform. This method applies the child transformation to the provided matrix.

  • RenderObject.transformTo chains paint transformation matrices mapping from the current coordinate space to the provided ancestor’s coordinate space. [WIP]

What other protocols are implemented in the framework?

  • RenderBox, RenderSliver.

Last updated