background

With the rapid growth of idle fish’s business, there are more and more requirements for operation, including many interface modifications or operation pits. The idle Fish version is now released every 2 weeks. How can we quickly iterate through the product and skip window periods to meet these needs? In addition, the package size of Xianyu client has also become very large. Compared with 2016, the package size of Android has increased nearly twice. How can the package size be reduced? The first thought is to solve such problems dynamically.

As for the native capabilities of the dynamic, Android platform companies have a good dynamic solution, and Google even provides Android App Bundles to enable developers to better support the dynamic. Because of Apple’s official concern about the risk of being dynamic, it is not very supportive. Therefore, dynamic capability will be combined with Web. From the Hybrid solution based on WebView at the beginning, to React Native and Weex combined with Native.

At the same time, with the spread of idle fish Flutter technology, there have been more than 10 pages implemented with Flutter. None of the methods mentioned above is suitable for the Flutter scene. How to solve the problem of dynamic Flutter?

The dynamic plan

We initially investigated Google’s dynamic solution CodePush.

01

CodePush is Google’s official dynamic solution. The Dart VM loads isolate_SNAPshot_data and isolate_SNAPshot_instr files during execution, and dynamically changes these files to achieve dynamic update. The official Flutter source code has been submitted for dynamic updates. For details, see ResourceExtractor.java. Currently, this feature is still under development. Look forward to ing.

02

Dynamic templates, such as LuaViewSDK, Tangram-ios, and Tangram-Android, are made dynamic by defining a SET of DSLS that parse dynamically create views on the side. These options are created Native views. If you want to implement them inside a Flutter, you need to create Texture to bridge them. After rendering Native, attach textures to the container of Flutter, which is expensive to implement and has questionable performance. It is not suitable for idle fish scenes.

Therefore, we proposed xianyu’s own scheme to make Flutter dynamic. A colleague has already introduced the principle of the scheme: “After more than 2 months of design and coding, I have combed the scheme comparison and best implementation of Flutter dynamic”. Here are the specific implementation details.

Template compilation

A customized DSL is costly to maintain. How can TEMPLATE delivery be implemented without a customized DSL? The idle fish solution is to convert the Dart file directly to a template so that the template file can also be precipitated quickly to the side.

01

This is a list structure, and each block is a separate Widget. Now we want to dynamically render the “sell to Fish” block. After splitting this block, we need 3 child controls: header, menu bar, and prompt bar. Because these three parts of the interface have some logic processing, they are built in first.

The built-in child controls are MenuTitleWidget, MenuItemWidget, and HintItemWidget, written with the following template:

                        
     

    @override

    Widget build(BuildContext context) {

    return new Container(

    child : new Column(

    children : < Widget>[

    New MenuTitleWidget (data), // header

    New Column (// menu bar

    children : < Widget>[

    new Row (

    children : < Widget>[

    new MenuItemWidget (data. menus[0 ]),

    new MenuItemWidget (data. menus[1 ]),

    new MenuItemWidget (data. menus[2 ]),

    ].

    )

    ].

    ),

    New Container (// Prompt bar

    child : new HintItemWidget( data.hints [0])),

    ].

    ),

    );

    }

Copy the code

With the style description omitted, you can see that writing a template file is just like writing a normal widget, with a few caveats:

  1. Each Widget needs to be decorated with new or const

  2. Data access starts with data, array access with [], and dictionary access with. access

After the template is written, it is necessary to consider how to render on the end. In earlier versions, the file is directly parsed on the end side, but for performance and stability, it is better to compile it in the early stage and then send it to the end side.

02

The Dart library Analyzer is used to compile the template. The parseCompilationUnit function parses the Dart source directly into an AST tree with CompilationUnit as the Root node, which contains the syntax and semantic information of the Dart source files. The next goal is to convert CompilationUnit to a JSON format.

The child node is ReturnStatementImpl template parsing out the build function, and it contains a child node InstanceCreationExpressionImpl, corresponding to the inside of the template newContainer (…). Of its children, the ones we care most about are the ConstructorNameImpl and ArgumentListImpl nodes. ConstructorNameImpl identifies the name of the created node, and ArgumentListImpl identifies the created parameters, which contain a list of arguments and variable arguments.

Define the following structure to store this information:


     

    class ConstructorNode {

    // Create the name of the node

    String constructorName;

    // Parameter list

    List<dynamic> argumentsList = <dynamic>[];

    // Variable parameters

    Map<String, dynamic> arguments = <String, dynamic>{};

    }

Copy the code

To recursively traverse the entire tree, we get a tree of ConstructorNode. The following code parses the parameters of 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;

    }

    / / / is function

    if ( namedExp.expression is FunctionExpression ) {

    currentNode .arguments[ name] =

    FunctionExpressionParser.parse (namedExp. expression);

    } else {

    /// is not a function

    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 );

    }

    }

Copy the code

After the end gets the ConstructorNode node tree, you can build a Widget tree based on the Widget names and parameters.

Rendering engine

Once the compiled template JSON is in hand, the end parses the template and creates the Widget. Let’s take a look at the framework and workflow of the project:

Workflow:

  1. The developer writes the DART file, compiles it and uploads it to the CDN

  2. The end – side gets the template list and stores it on the end – side

  3. The service side delivers the template ID and template data

  4. The template is then bridged to the Flutter side and the Widget tree is created

For Native test, it is mainly responsible for template management and output to the Flutter side by bridging.

01

Template acquisition is divided into two parts, Native part and Flutter part. Native is responsible for template management, including download, downgrade, caching, and so on.

When the program is started, the template list will be obtained first, and the business side needs to implement it by itself. The template list obtained by the Native layer will be stored in the local database first. When the business code on the Flutter side uses the template, it will bridge to obtain the template information, that is, the JSON format we mentioned before, which will also be cached by Flutter to reduce the interaction between Flutter and Native.

02

When the Flutter side gets the JSON format, it first parses the ConstructorNode tree and then recursively creates the Widget.

The process of creating each Widget is to parse the argumentsList and arguments in the node and do the data binding. For example, creating HintItemWidget requires the data content to be passed in as a hint, and newHintItemWidget(data.hints[0]), when parsing argumentsList, parses specific values from the raw data in a key-path manner.

The parsed values are stored in WidgetCreateParam, and each widget can parse the required parameters from WidgetCreateParam as it recursively traverses each created node.

                                                
     

    /// Parameters used to build the widget

    class WidgetCreateParam {

    String constructorName; /// Build name

    dynamic context; /// Build context

    Map arguments = {}; /// dictionary parameters

    List argumentsList = []; /// List parameters

    dynamic data; /// raw data

    }

Copy the code

Using the above logic, we can convert the ConstructorNode tree into a Widget tree that can be rendered by the Flutter Framework.

Now that we have resolved the template and rendered it to the interface, how should interactive events be handled?

03

When writing interactions, GestureDector, InkWell, etc., are used to handle click events. How can interactive events be made dynamic?

Take the InkWell component as an example. Define its onTap function as openURL(data.hints[0].href,data.hints[0].params). When InkWell is created, OpenURL will be used as the event ID to find the corresponding handler function. When the user clicks, the corresponding parameter list will be parsed and passed, with the following code:

                                                        
     

    .

    final List tList = [];

    // Parse out the argument list

    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);

    }

    });

    // Find the corresponding handler

    final dynamic handler =

    TeslaEventManager. sharedInstance(). eventHandler( exp. actionName);

    if (handler ! = null) {

    handler (tList);

    }

    .

Copy the code

The effect

After the new version of my page added dynamic rendering capability, if there is a need to add a new component type, can directly compile publish template, server to send new data content, can render out; Now that you have dynamic capability, you’re going to care about rendering performance.

01

After adding dynamic loading logic, two dynamic cards have been opened. The following is the frame rate data of the new version of my page in nearly half a month:

As can be seen from the figure above, the frame rate has not decreased, basically keeping around 55-60 frames. You can add more dynamic cards later to observe the effect.

Note: Because my page will have some local business judgment, from other pages back to my TAB, will refresh the interface, so the framerate will be wasted.

Implementationally, since each card needs to be traversed through the ConstructorNode tree to create, and each build needs to parse out the parameters inside, this can be optimized for things like caching the same widgets, mapping the data content and doing data binding.

02

Now that the rendering logic is monitored, an Error is actively thrown if there is no local Widget creation function. According to the monitoring data, there is no abnormal situation in the rendering process, and error burying points need to be added to the bridge layer and native layer later.

The follow-up plan

Based on the Flutter dynamic template, all the Flutter requirements that previously needed to evolve can be dynamically changed. The above logic is based on the native Flutter system, which has low learning and maintenance costs. Dynamic code can also be precipitated quickly to the end.

In addition, UI2Code is studying the dark technology of UI2Code. For those who are not familiar with it, you can refer to this article titled “A series of important articles! UI2Code intelligently generates Flutter code — An overall design article”. We can imagine that if there is a requirement for dynamic display of a component, UED produces a visual draft, converts it into Dart file through UI2Code, and then converts it into a dynamic template through this system. When it is delivered to the end side, it can be directly rendered. Programmers do not need to write code, so as to achieve automatic operation. It seems likely that programmers will lose their jobs in the future.

The Widgets based on Flutter can also be expanded to include more personalized components, such as built-in animation components, which can dynamically deliver animation, and more fun things to explore together.

reference

  1. https://github.com/flutter/flutter/issues/14330

  2. https://www.dartlang.org/

  3. https://mp.weixin.qq.com/s/4s6MaiuW4VoHr7f0SvuQ

  4. https://github.com/flutter/engine

Has the Flutter behind open source | 200 million user application framework Fish story

Blockbuster series | “UI2Code” intelligent generating code Flutter

Old code more = over-coupling =if else? “Engineer Ali said