Flutter Analysis and Practice: Design of Lightweight Dynamic Rendering Engine

Image for post
Image for post

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 and 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.

Image for post
Image for post
Figure 3–6

The built-in sub-controls are , , and . 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 function can be used to parse the Dart source code in the Abstract Syntax Tree (AST) whose root node is . It contains the syntax and semantic information of Dart source files, as shown in Figure 3-7. Next, we will try to convert into a JSON format file.

Image for post
Image for post
Figure 3–7

The child node of the build function parsed from the preceding template is . It contains the child node, corresponding to new in the template. The and child nodes are the most important. specifies the name of the creation node. 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 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 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.

Image for post
Image for post
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

Image for post
Image for post
Figure 3–9

3.2.4.2 Widget Creation

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

Image for post
Image for post
Figure 3–10

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

Image for post
Image for post
Figure 3–11

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

/// 构建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 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, and are used to process tap events. The processing logic is a function. To implement dynamic transformation, perform the following operations:

Use as an example. Define the function of as . The parsing logic parses it into an event with as the ID. The Flutter side provides an event processing mapping table. When you tap , 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.

Image for post
Image for post
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 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.

Original Source:

Written by

Follow me to keep abreast with the latest technology news, industry insights, and developer trends.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store