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.
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?
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.
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.
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.
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.
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.
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.
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.
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.
Figure 3-8
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.
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.
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.
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.
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?
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);
}
...
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.
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.
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.
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.
Flutter Analysis and Practice: App Framework Design Practices
56 posts | 4 followers
FollowXianYu Tech - September 9, 2020
Alibaba Clouder - September 21, 2020
Alibaba Clouder - February 8, 2021
Alibaba Clouder - December 22, 2020
Alibaba Tech - February 24, 2020
XianYu Tech - September 11, 2020
Is this (dynamic template based ui) allowed for IOS apps? This seems to violate Apple's terms (no dynamic code except for javascript)
56 posts | 4 followers
FollowHelp enterprises build high-quality, stable mobile apps
Learn MoreAn enterprise-level continuous delivery tool.
Learn MoreProvides comprehensive quality assurance for the release of your apps.
Learn MoreAlibaba Cloud (in partnership with Whale Cloud) helps telcos build an all-in-one telecommunication and digital lifestyle platform based on DingTalk.
Learn MoreMore Posts by XianYu Tech
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