Recently, the team tried to use group DinamicX’s DSL to achieve dynamic template rendering on Flutter side by delivering DSL templates. Once the performance issues were resolved, a new challenge arose — rendering consistency. How to greatly improve the rendering consistency between Flutter and Native without compromising rendering performance?

Train of thought

In the first version of the rendering architecture design, we used a composite solution to complete the transformation from DSL to Widget with the Widget as the center. This went well in the early days, but as the complexity of the template increased, there were some Bad cases.

After analyzing these Bad cases, we found that these Bad cases could not be completely solved under the initial rendering architecture, mainly for the following two reasons:

1. We use Stack to represent FrameLayout and Column/Row to represent LinearLayout. They seem to have similar functions, but in fact their internal implementations are quite different, causing many Bad cases that are difficult to solve in the process of use.

2. The first version tried to have a preliminary understanding of the layout concept of DSL through custom widgets, but failed to achieve complete alignment, so Bad Case could not be systematically solved.

To fundamentally solve these problems, a new rendering architecture solution needs to be redesigned to fully understand and align the LAYOUT concepts of the DSL.

New rendering architecture design

Since DinamicX’s DSL is very similar to Android XML, we will introduce its layout concept using Android’s Measure mechanism. As many of you know, in Android Measure, a parent View calculates its child’s MeasureSpecMode according to its own child View’s MeasureSpecMode and its child View’s LayoutParams. The calculation table is UNSPECIFIED, ignoring MeasureSpecMode:

We can calculate whether the width/height of each DSL Node is EXACTLY or AT_MOST based on the table above. To understand the DynamicX DSL, Flutter needs to introduce the concept of MeasureSpecMode. Since the initial rendering architecture was widget-centered, it was difficult to introduce the concept of MeasureSpecMode, so RenderObject was needed to redesign the rendering architecture.

Based on the RenderObject layer, a new rendering architecture is designed. In the new rendering architecture, each DSL Node is converted into a subtree on the RenderObject Tree, which consists of three main parts.

  • Decoration: Decoration is used to support background colors, borders, rounded corners, touch events, etc., which can be combined.

  • Render layer: The Render layer is used to express the layout rules and sizes of nodes after transformation.

  • The Content layer: The Content layer is responsible for displaying specific Content. For layout controls, the Content is their children, while for non-layout controls, such as TextView and ImageView, the Content will be expressed by RenderParagraph and RenderImage in Flutter.

Render layer is the core layer in our new rendering architecture, which is used to express the layout rules and size after Node transformation, and plays a key role in understanding the concept of DSL layout. Its class diagram is as follows:

DXRenderBox is the base class for all controls Render layer and its derived for two classes: DXSingleChildLayoutRender and DXMultiChildLayoutRender. DXSingleChildLayoutRender of them Render all the layout of the control layer of the base class, while the DXMultiChildLayoutRender is the base class for all layout controls Render layers.

For non-layout controls, the Render layer only affects the size, not the internal display content, so in theory View, ImageView, Switch, Checkbox, etc., are expressed the same in the Render layer. DXContainerRender is the implementation class that expresses these non-layout controls. In this case, DXTextContainerRender is designed separately because TextView has a maxWidth attribute that affects its size and requires special handling of the vertical center of text.

For layout controls, different layout controls represent different layout rules, so different layout controls generate different implementation classes in the Render layer. DXLinearLayoutRender and DXFrameLayoutRender are used to express the layout rules of LinearLayout and FrameLayout respectively.

New rendering architecture implementation

With the new rendering architecture design completed, we can start designing the base class DXRenderBox. For DXRenderBox, we need to implement three methods that are critical to Flutter Layout: sizedByParent, performResize, and performLayout.

The principle of Flutter Layout

We will briefly review the principle of Flutter Layout. Since many previous articles have introduced the principle of Flutter Layout, this time we will focus directly on the part of Flutter Layout used to calculate the size of the RenderObject.

In the process of Flutter Layout, the most important thing is to determine the size of each RenderObject. This is done in the Layout method of the RenderObject. The Layout method does two things:

1. Determine the relayoutBoundary corresponding to the current RenderObject

2. Call performResize or performLayout to determine your size

In order to facilitate readers to read the layout method has been simplified, the code is as follows:


     

    abstractclassRenderObject{

    Constraintsget constraints => _constraints;

    Constraints _constraints;

    boolget sizedByParent => false;

    void layout(Constraints constraints, { bool parentUsesSize = false}) {

    / / relayoutBoundary calculation

    .

    //layout

    _constraints = constraints;

    if(sizedByParent) {

    performResize();

    }

    performLayout();

    .

    }

    }

Copy the code

It can be said that as long as the layout method is mastered, the process of Flutter layout is basically mastered. Let’s briefly analyze the Layout method.

The constraints parameter represents the constraints passed in by parent, and the resulting size of the RenderObject must conform to this constraint. The parentUsesSize parameter indicates whether the parent will use the size of the child. It is used to calculate the repaintBoundary and can be used to optimize the Layout process.

SizedByParent is a property of the RenderObject. By default, it’s false. Subclasses can override this property. As the name implies, sizedByParent indicates that the calculation of the size of the RenderObject is entirely determined by its parent. In other words, the size of the RenderObject is related only to the constraints given by the parent, not to sizes of its children.

Also, sizedByParent determines in which method the size of the RenderObject needs to be determined. If sizedByParent is true, then size must be determined in the performResize method. Otherwise, size needs to be determined in performLayout.

The function of the performResize method is to determine the size of the RenderObject based on the constraints passed in by the parent.

In addition to determining size, performLayout is responsible for iterating through the child-. Layout method to evaluate sizes and offsets of children.

How do I implement sizedByParent

When sizedByParent is true, it means that the size of the RenderObject is independent of children. So in our DXRenderBox, sizedByParent can only be set to true if both widthMeasureMode and heightMeasureMode are DX_EXACTLY.

The nodeData type in the code is DXWidgetNode, which represents the DSL Node mentioned above, while widthMeasureMode and heightMeasureMode represent the MeasureSpecMode corresponding to the width and height of the DSL Node, respectively.


     

    abstractclassDXRenderBoxextendsRenderBox{

    DXRenderBox({@requiredthis.nodeData});

    DXWidgetNode nodeData;

    @override

    boolget sizedByParent {

    return nodeData.widthMeasureMode == DXMeasureMode.DX_EXACTLY &&

    nodeData.heightMeasureMode == DXMeasureMode.DX_EXACTLY;

    }

    .

    }

Copy the code

How to implement performResize

The performResize method is called only if sizedByParent is true, i.e. widthMeasureMode and heightMeasureMode are both DXEXACTLY. If widthMeasureMode and heightMeasureMode are both DXEXACTLY, then nodeData’s width and height are either specific or match parent, So in the performResize method you only need to deal with the specific width/height or matchparent. If the width/height has a specific value, it is match_parent. If there is no specific value, it is the maximum value of constraints.


     

    abstractclassDXRenderBoxextendsRenderBox{

    .

    @override

    void performResize() {

    double width = nodeData.width ?? constraints.maxWidth;

    double height = nodeData.height ?? constraints.maxHeight;

    size = constraints.constrain(Size(width, height));

    }

    .

    }

Copy the code

How to implement performLayout for non-layout space

DXRenderBox is the base class for the Render layer of all controls without implementing performLayout. Different DXRenderBox subclasses have different performLayout methods. This method is also key to Flutter’s understanding of DSL. Next DXSingleChildLayoutRender as example to illustrate the implementation of performLayout train of thought.

The main function is to determine the layout of DXSingleChildLayoutRender controls the size of the. For example, how big an ImageView is, that’s how you determine it.


     

    abstractclassDXSingleChildLayoutRenderextendsDXRenderBox

    withRenderObjectWithChildMixin<RenderBox> {

    @override

    void performLayout() {

    BoxConstraints childBoxConstraints = computeChildBoxConstraints();

    if(sizedByParent) {

    child.layout(childBoxConstraints);

    } else{

    child.layout(childBoxConstraints, parentUsesSize: true);

    size = defaultComputeSize(child.size);

    }

    }

    .

    }

Copy the code

First, we calculate childBoxConstraints. And then figure out if it’s sizedByParent. If so, its size has already been calculated in the performResize stage, at which point you just need to call the child.Layout method. Otherwise, you need to set the parentUsesSize parameter to true when calling child.layout and calculate its size from child.size. How to calculate size from child.size?


     

    Size defaultComputeSize(Size intrinsicSize) {

    double finalWidth = nodeData.width ?? constraints.maxWidth;

    double finalHeight = nodeData.height ?? constraints.maxHeight;

    if(nodeData.widthMeasureMode == DXMeasureMode.DX_AT_MOST) {

    finalWidth = intrinsicSize.width;

    }

    if(nodeData.heightMeasureMode == DXMeasureMode.DX_AT_MOST) {

    finalHeight = intrinsicSize.height;

    }

    return constraints.constrain(Size(finalWidth,finalHeight));

    }

Copy the code
  • If the width/height measude-dxexactly, then the final width/height measude-is matchparent, and if it is not, then it is the maximum of constraints.

  • If a measureMode is DX_ATMOST, then the width/height is the same as the child’s width/height.

How to implement performLayout for layout space

Layout controls in performLayout need to determine their own size, but also need to design their own layout rules. Take FrameLayout as an example to illustrate how to implement performLayout for the layout control.


     

    classDXFrameLayoutRenderextendsDXMultiChildLayoutRender{

    @override

    void performLayout() {

    BoxConstraints childrenBoxConstraints = computeChildBoxConstraints();

    Double maxWidth = 0.0;

    Double maxHeight = 0.0;

    //layout children

    visitDXChildren((RenderBox child,int index,DXWidgetNode childNodeData,DXMultiChildLayoutParentData childParentData) {

    if(sizedByParent) {

    child.layout(childrenBoxConstraints,parentUsesSize: true);

    } else{

    child.layout(childrenBoxConstraints,parentUsesSize: true);

    maxWidth = max(maxWidth,child.size.width);

    maxHeight = max(maxHeight,child.size.height);

    }

    });

    //compute size

    if(! sizedByParent) {

    size = defaultComputeSize(Size(maxWidth, maxHeight));

    }

    //compute children offsets

    visitDXChildren((RenderBox child,int index,DXWidgetNode childNodeData,DXMultiChildLayoutParentData childParentData) {

    Alignment alignment = DXRenderCommon.gravityToAlignment(childNodeData.gravity ?? nodeData.childGravity);

    childParentData.offset = alignment.alongOffset(size - child.size);

    });

    }

    }

Copy the code

The FrameLayout layout process can be divided into three parts

1. Layout all the children, if FrameLayoutRender is not sizedByParent, you need to calculate the maximum width and height of all the children at the same time to calculate their own size.

2. Calculate its own size. See the previous section for the calculation scheme defaultComputeSize

3. Gravity was converted into alignment, and offsets of all children were calculated.

Look at the FrameLayout layout process, do you think it is very simple? However, it should be pointed out that the code of FrameLayoutRender above will encounter some Bad cases, among which the classic problem is that the width/height of FrameLayout is MatchContent, while the width/height of its children are matchparent. This situation can be “measured” twice on the same child on Android. How can this be done on Flutter?

How does Flutter implement twice measure?

Let’s start with an example:

The LinearLayout in the figure above is a vertical LinearLayout, width is set to matchcontent, it contains two textviews, width is both matchparent, so in this example, what should the layout process be?

First of all, measure the width of two TextViews, MeasureSpecMode is AT_MOST. Simply speaking, ask them how wide they need to be. The LinearLayout then sets the maximum width required by the two TextViews to its own width. Finally, measure the two TextViews a second time, at which MeasureSpecMode will be changed to Exactly, and MeasureSpecSize is the width of the LinearLayout.

The common layout process of Flutter is as follows:

  • PerformResize calculates the size of children sizes using child-.layout

  • Determine child sizes via child-.layout and then calculate the size of children sizes

None of the above will do the job we want in this example. We need to find a way to know the width and height of the child before we call Child.layout. Finally we find that getMinIntrinsicWidth, getMaxIntrinsicWidth, getMinIntrinsicHeight and getMaxIntrinsicHeight satisfy us. Use getMaxIntrinsicHeight as an example to illustrate the use of these methods.


     

    double getMaxIntrinsicWidth(double height) {

    return _computeIntrinsicDimension(_IntrinsicDimension.maxWidth, height, computeMaxIntrinsicWidth);

    }

Copy the code

GetMaxIntrinsicWidth receives a height argument that determines what maxIntrinsicWidth should be when height is this value. This method will eventually compute the maxIntrinsicWidth using the computeMaxIntrinsicWidth method, and the results will be saved. If overridden, the getMaxIntrinsicWidth method should not be overridden, but the computeMaxIntrinsicWidth method. It is important to note that these methods are not lightweight and should only be used when you really need them.

Perhaps you can not help but ask, these methods to calculate the width and height accuracy? In fact, every RenderBox subclass needs to ensure that these methods are correct. For example, the RenderParagraph used to display text implements these compute methods and thus gets the width of the RenderParagraph before it is laid out.

We design the Render layer in the class have to compute method, these methods to implement is not complicated, or DXSingleChildLayoutRender as examples to illustrate how to implement these methods.


     

    @override

    double computeMaxIntrinsicWidth(double height) {

    if(nodeData.width ! = null) {

    return nodeData.width;

    }

    if(child ! = null) return child.getMaxIntrinsicWidth(height);

    Return0.0;

    }

Copy the code

The above code is relatively simple and will not be repeated.

So we will have a simple look at the example of the problem — first by the child. GetMaxIntrinsicWidth to calculate the width of each child needs. Then the maximum of these widths is used to determine the width of the LinearLayout. Finally, each child is laid out using child.layout. The maxWidth and minWidth of the constraints passed in are both the width of the LinearLayout.

The effect

The new rendering architecture enables Flutter to understand and align with the layout concepts of DSL, systematically solving the Bad cases encountered before, and bringing more possibilities to Flutter dynamic template solutions.

The rendering performance of the new and old versions has been tested and compared, and it can be found that the rendering performance of dynamic templates has been further improved by comparing page rendering time and FPS under the new rendering architecture.

Follow-up prospects

After the upgrade of the rendering architecture, we solved all the Bad cases we had encountered and provided a strong starting point for systematic analysis to solve these problems. We also further improved the rendering performance, which made Flutter dynamic template rendering possible. In the future, we will continue to improve this solution to achieve technology enabling business.

reference

https://flutter.dev/docs/resources/inside-flutter

The Idle Fish Team is the industry leader in the new technology of Flutter+Dart FaaS integration, right now! Client/server Java/architecture/front-end/quality engineer recruitment for the society, base Hangzhou Alibaba Xixi Park, together to create creative space community products, do the depth of the top open source projects, together to expand the technical boundary achievement!

* Send resumes to small idle fish →[email protected]