×
Community Blog Flutter Analysis and Practice: Design of Lightweight Dynamic Rendering Engine

Flutter Analysis and Practice: Design of Lightweight Dynamic Rendering Engine

With the rapid growth of Xianyu's businesses, how can products be quickly iterated to skip the window period for these demands?

With the booming development of mobile smart devices, a mobile multi-terminal development framework has become a general trend. Download the Flutter Analysis and Practice: Evolution and Innovation of Xianyu Technologies eBook for step-by-step analyses based on real-life problems, as well as clear explanations of the important concepts of Flutter.

3.2.1 Background

With the rapid growth of Xianyu's businesses, its operation demands are increasing, including many UI modifications and presentation placement demands. How can products be quickly iterated to skip the window period for these demands? In addition, the Xianyu client has a large package body. Compared to siz aes in 2016, the current size of the Android Package Kit (APK) has almost doubled. How can we reduce the size of the APK? First, we try to dynamically solve these problems.

Companies on the Android platform have comprehensive dynamic solutions to implement dynamic native capabilities. Google also provides Android App Bundles to allow developers to better support the dynamic transformation. Apple does not support this, due to concern about the risks of dynamic transformation. Therefore, we consider how to integrate the dynamic capabilities with the web, including the initial WebView-based hybrid solution and the existing React Native and Weex solutions.

Meanwhile, as Xianyu's Flutter technology is more widely used, more than 10 Flutter pages have been implemented and the demands for dynamic Flutter are also increasing. However, none of the preceding methods are suitable for Flutter scenarios. How can this problem be solved?

3.2.2 Dynamic Solution

3.2.2.1 CodePush

CodePush is a dynamic solution provided by Google. It implements dynamic updates by loading the isolate_snapshot_data and isolate_snapshot_instr files when a Dart virtual machine (VM) is run. A solution is available in the Flutter official source code.

3.2.2.2 Dynamic Template

By defining a domain specific language (DSL), dynamic templates are used to write corresponding parsing engines on the client side to implement dynamic solutions, such as LuaViewSDK, Tangram-iOS, and Tangram-Android. These solutions create native views. To implement views in Flutter, textures must be created. After rendering is completed on the native side, textures are pasted into the Flutter container. However, due to its high cost and uncertain performance, this method is not suitable for Xianyu.

Therefore, Xianyu proposes its own Flutter dynamic solution.

3.2.3 Template Compilation

The maintenance cost for customizing a DSL is relatively high. How can we implement dynamic loading without customizing a DSL? Xianyu directly uses Dart files as templates and converts them into JSON protocol data. The client side obtains and parses the protocol data. This allows the Dart template files to be quickly integrated to the client side for secondary development.

3.2.3.1 Template Specifications

This section uses "My Page" of the latest version as an example to describe the complete template file. This is a list structure, with each block being an independent widget. Now, we want to dynamically render the "Sold on Xianyu" block. After splitting this block, we need three sub-controls: head, menu bar, and prompt bar, as shown in Figure 3-6. These components have specified business logic and cannot be delivered dynamically. The logic must be built into the components.

1
Figure 3-6

The built-in sub-controls are MenuTitleWidget, MenuItemWidget, and HintItemWidget. The templates are:

@override
Widget build(BuildContext context) {
    return new Container(
        child: new Column(
            children: <Widget>[
                new MenuTitleWidget(data),    // 头部
                new Column(    // 菜单栏
                    children: <Widget>[
                        new Row(
                            children: <Widget>[
                                new MenuItemWidget(data.menus[0]),
                                new MenuItemWidget(data.menus[1]),
                                new MenuItemWidget(data.menus[2]),
                            ],
                        )
                    ],
                ),
                new Container(    // 提示栏
                    child: new HintItemWidget(data.hints[0])),
            ],
        ),
    );
}

The style description is omitted here. The method of writing a template file is the same as writing a common widget. However, each widget needs to be modified with new or const, data access must start with "data," data arrays are accessed in the form of "[]," and dictionaries are accessed in the form of "."

After writing the template, we must consider how to render it on the client. In an earlier version, files were parsed on the client side. However, to ensure performance and stability, we had to compile these files before sending them to the client side.

3.2.3.2 Compilation Process

The Analyzer library of Dart is used to compile templates. The parseCompilationUnit function can be used to parse the Dart source code in the Abstract Syntax Tree (AST) whose root node is CompilationUnit. It contains the syntax and semantic information of Dart source files, as shown in Figure 3-7. Next, we will try to convert CompilationUnit into a JSON format file.

2
Figure 3-7

The child node of the build function parsed from the preceding template is ReturnStatementImpl. It contains the InstanceCreationExpressionImpl child node, corresponding to new Container(...) in the template. The ConstructorNameImpl and ArgumentListImpl child nodes are the most important. ConstructorNameImpl specifies the name of the creation node. ArgumentListImpl specifies the creation parameters, including the parameter list and variables.

The following struct is defined to store the information:

class ConstructorNode {
    // 创建节点的名称
    String constructorName;
    // 参数列表
    List<dynamic> argumentsList = <dynamic>[];
    // 变量参数
    Map<String, dynamic> arguments = <String, dynamic>{};
}

You can retrieve a ConstructorNode tree by recursively traversing the entire tree. The following code provides an example of how to parse a single node:

ArgumentList argumentList = astNode;

for (Expression exp in argumentList.arguments) {
    if (exp is NamedExpression) {
        NamedExpression namedExp = exp;
        final String name = ASTUtils.getNodeString(namedExp.name);
        if (name == 'children') {
            continue;
        }

        /// 是函数
        if (namedExp.expression is FunctionExpression) {
            currentNode.arguments[name] =
                FunctionExpressionParser.parse(namedExp.expression);
        } else {
            /// 不是函数
            currentNode.arguments[name] =
                ASTUtils.getNodeString(namedExp.expression);
        }
    } else if (exp is PropertyAccess) {
        PropertyAccess propertyAccess = exp;
        final String name = ASTUtils.getNodeString(propertyAccess);
        currentNode.argumentsList.add(name);
    } else if (exp is StringInterpolation) {
        StringInterpolation stringInterpolation = exp;
        final String name = ASTUtils.getNodeString(stringInterpolation);
        currentNode.argumentsList.add(name);
    } else if (exp is IntegerLiteral) {
        final IntegerLiteral integerLiteral = exp;
        currentNode.argumentsList.add(integerLiteral.value);
    } else {
        final String name = ASTUtils.getNodeString(exp);
        currentNode.argumentsList.add(name);
    }
}

After the ConstructorNode node tree is obtained, a widget tree is generated based on the widget name and parameters.

3.2.4 Rendering Engine

The client side obtains the template information in JSON format. The rendering engine parses the template information and creates widgets. Figure 3-8 shows the overall framework and workflow of the project.

3
Figure 3-8

  • Developers compile and upload Dart files to Alibaba Cloud Content Delivery Network (CDN.)
  • The client side obtains the template list and stores it.
  • The business side delivers the template ID and template data.
  • The Flutter side obtains the template and creates a widget tree.
  • The native side manages the template and outputs the template to the Flutter side.

3.2.4.1 Template Obtaining

Native and Flutter are involved in obtaining a template. Native is mainly responsible for template management, including downloading, downgrading, and caching, as shown in Figure 3-9.

4
Figure 3-9

After the app is started, the business side obtains the template list through native. After obtaining the template list, the native layer stores it in the local database. When the Flutter business code requires templates, Flutter obtains the template information, that is, JSON files. Flutter also caches some information to reduce the interaction between Flutter and native.

3.2.4.2 Widget Creation

After obtaining the JSON files, the Flutter side parses the files to obtain the ConstructorNode tree, and recursively creates widgets, as shown in Figure 3-10.

5
Figure 3-10

The process of creating each widget is to parse the argumentsList and arguments in the node and bind data. For example, when you create HintItemWidget, new HintItemWidget(data.hints[0]) needs to be imported. When argumentsList is parsed, a specific value is obtained from the raw data in key-path format, as shown in Figure 3-11.

6
Figure 3-11

All the obtained values are stored in WidgetCreateParam. When each creation node is recursively traversed, each widget can parse the required parameters from the WidgetCreateParam.

/// 构建Widget用的参数
class WidgetCreateParam {
  String constructorName;    /// 构建的名称
  dynamic context;    /// 构建的上下文
  Map<String, dynamic> arguments = <String, dynamic>{}; /// 字典参数
  List<dynamic> argumentsList = <dynamic>[]; /// 列表参数
  dynamic data; /// 原始数据
}

Based on the preceding logic, the ConstructorNode tree can be converted into a widget tree before rendered by the Flutter Framework.

Now, the template can be parsed and rendered on UIs. Then, how should interaction events be handled?

3.2.4.3 Event Processing

Generally, during UI interaction, GestureDector and InkWell are used to process tap events. The processing logic is a function. To implement dynamic transformation, perform the following operations:

Use InkWell as an example. Define the onTap function of InkWell as openURL(data.hints[0]. href, data.hints[0].params). The parsing logic parses it into an event with OpenURL as the ID. The Flutter side provides an event processing mapping table. When you tap InkWell, the corresponding processing function is located, and the corresponding parameter list is obtained and transmitted. The code is:

...
final List<dynamic> tList = <dynamic>[];
// 解析出参数列表
exp.argumentsList.forEach((dynamic arg) {
    if (arg is String) {
        final dynamic value = valueFromPath(arg, param.data);
        if (value != null) {
            tList.add(value);
        } else {
            tList.add(arg);
        }
    } else {
        tList.add(arg);
    }
});

// 找到对应的处理函数
final dynamic handler =
    TeslaEventManager.sharedInstance().eventHandler(exp.actionName);
if (handler != null) {
    handler(tList);
}
...

3.2.5 Final Effect

After the dynamic rendering capability is added to "My Page" of the latest version, if you need to add a new component type, you can directly compile and publish the template, and the server will issue the new data content for rendering. Along with dynamic capabilities, you may also care about rendering performance.

3.2.5.1 Frame Rate

After the dynamic rendering capability is added, two dynamic cards have been provided. Figure 3-12 shows the frame rate data of the latest version of "My Page" within the past half month.

7
Figure 3-12

As shown in Figure 3-12, the frame rate remains at 55 to 60. More dynamic cards can be added to check the effect.

Note: "My Page" has some local business judgment. When you return to "My Page," it is refreshed, which reduces the frame rate.

In terms of implementation, each card needs to be created by traversing the ConstructorNode tree, and the parameters must be parsed for each creation, which can be optimized. For example, if the same widgets are cached, only the data needs to be mapped and bound.

3.2.5.2 Failure Rate

An error is returned if no local functions can be used to create widgets. According to the monitoring data, no exceptions have occurred in the rendering process, and error tracking needs to be added to the connection layer and the native layer.

Based on the Flutter dynamic template, changes can be made to Flutter dynamically, instead of being incorporated in the releases. The preceding logic is based on the Flutter native system, with low learning and maintenance costs, and dynamic code can be quickly integrated to the client side.

In addition, Xianyu is working on UI2CODE. If a component needs to be displayed dynamically, the user experience designer has already made a visual draft, converted it into a Dart file through UI2CODE, and then converted the Dart file into a dynamic template in this system. Then, the template is delivered to and rendered directly on the client side.

Based on Flutter widgets, more personalized components can be extended. For example, a built-in animation component can be used to deliver an animation dynamically.

2 2 1
Share on

XianYu Tech

56 posts | 4 followers

You may also like

Comments

5078500387488475 September 18, 2020 at 12:07 am

Would a project like this be valuable in solving the same problem, though perhaps in a slightly more elegant way? https://github.com/chgibb/hydro-sdk

5009021056677605 May 15, 2021 at 5:33 am

Is this (dynamic template based ui) allowed for IOS apps? This seems to violate Apple's terms (no dynamic code except for javascript)