Fair 2.0 logic dynamic architecture design and implementation

In Fair 1.0, we implemented the dynamic layout, but the data part has no dynamic ability. In response to this problem, we launched 2.0 to support logic dynamic. Fair logic is dynamic, we basically realize the logic processing ability related to data (the logic here is not just logic operation, including logic processing ability such as operation, branch, loop, etc.). Next, we will give a detailed introduction according to the location of the logical expression and the processing ability of different data types.

The core content of this article includes:

Data logic processing

Logic processing in layout

Flutter type data processing

Data logic processing

Every Flutter interface we touch is mostly composed of code related to layout and logic. (We focus on the interface of StatefulWidget) Such as the Counting Demo of the initial project of Flutter:

class _MyHomePageState extends State<MyHomePage> {

  // 变量 
  int _counter = 0; 

  // 方法
  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title), // 外部参数
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed this many times:',
            ),
            Text(
              '$_counter', // 变量使用
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionBut: FloatingActionBut(
        onPressed: _incrementCounter, // 事件回调
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}

As above, the annotation marks the data in the page and the data processing logic part. The logic dynamic of Fair is to realize the script-level dynamic analysis of this part of variables, methods, etc., as well as the binding of DSL layout attributes and the invocation of subsequent user operation event methods.

How to turn the logic in the Dart file into dynamically runnable variables and related operations? At the beginning of the project, we also discussed several solutions and tried to develop demos. For example, the Dart file is directly delivered, and the analysis and data binding of the layout and logic are completed on the pure Dart side. This operation is similar to developing a Dart Script engine, and the development cost is high. We finally decided: develop the Dart2JS tool, convert the logic part of the interface Dart file into JS script, and then use the native JSCore to complete the dynamic operation of the logic script, so our final structure is as follows:

1629596764485.jpg

We have realized that an interface file (Widget) marked with @FairPatch annotation is compiled by the Fair-Compiler tool to generate a DSL layout file and a JS logic file. Why do we need to use FairPatch annotations, which are generated after annotation? This is because our overall design concept is that Flutter's native interface and dynamic interface can be converted using a source file, and dynamic is only used in temporary scenarios such as urgent needs or A/B testing with uncertain needs. After the demand is stable, the precipitation source The file can continue to be used with the version, maximizing the performance of Flutter.

The dynamic script converted by Widget is shown in the following figure. For the specific principles, please refer to "Fair Distributed Product--DSL Generation Principle and Implementation" and "Fair Distributed Product--Dart2JS Generation Principle and Implementation"

image3.png

In the product generation process, in order to support logic dynamism, we need to translate the original Dart grammar a lot. I take the _counter variable in the Demo example as an example, it is a basic int type. Used in the following code:

Text('$_counter')

Although it seems that the statement is short, when generating Text Widget, the stage of data binding needs to be carefully distinguished. The following is the scene where num is converted to string and then passed parameters:

"className": "Text",
"pa": [
  "#($_counter)"
],

The core difficulty of logic dynamic is how to deal with variables. Variables in Flutter can be data variables or method variables. According to the scene in the Demo, let me list it (we combined the variable expressions in the same processing method according to the needs of data binding):

String cast base variable

Text('$_counter') => #($_counter)

widget pass parameter

Text(widget.title) => #(widget.title)

Method variable (callback execution)

onPressed: _incrementCounter => @(incrementCounter)

The variables and logic methods in these Flutter codes are transformed into the identifiers in the DSL file after being translated by the Fair compilation tool. The variables and method logic will eventually be implemented in the JS engine. Of course, when generating the corresponding interface from the layout DSL, we need to implement the data binding module. The data binding process is to complete the variable processing requirements on the Flutter side and the JS side according to the different expressions such as #( ) and @( ) mentioned above, and finally complete the generation of the target Widget.

Dart and JS dual domain variable mapping

Fair framework design: The required variable value query and variable processing method calls are only initiated from the Dart side, and the JS side is basically only used for data value storage and data logic-related operations.image4.png

The MVVM in Fair relies on the native mode of Flutter. As shown in the figure above, the data in the JS domain is synchronized to the Dart domain. It only needs to call the familiar setState on the JS side. Of course, this part is unaware of developers who use the Fair framework, and the compilation tools help us complete the relevant conversions. The native code and the generated JS code are compared as follows:

JS:
_incrementCounter: function _incrementCounter() {
    const __thiz__ = this;
    with (__thiz__) {
        setState('#FairKey#', function dummy() {
            _counter++;
        });
    }
},
Dart:
void _incrementCounter() {
  setState(() {
    _counter++;
  });
}

As you can see, there is not much difference in other codes except the with of JS to simplify the access domain and the FairKey required by the communication target object. For specific communication design, please refer to "Fair Logic Dynamic Communication Implementation".

The life cycle:

The above describes how the variable values ​​of the dual fields are mapped. Some students may ask, how is the life cycle of Dart and JS synchronized? (Fair provides the StatefulWidget carrier, so only the correspondence between the StatefulWidget life cycle and the JS life cycle is introduced here)image5.png

As shown above, Fair provides 2 aware lifecycle callbacks to the JS domain. onLoad is to notify the JS side to initialize variables after the JS is loaded; onUnload is to notify the JS side to recycle resources when the page disappears.

Minimal render:

The value variable of the Widget property in the Fair will generate the corresponding ValueNotifier object to achieve local component rendering. Computational variables, such as method parameters, we do not do special treatment. For variables participating in the Widget hierarchy management, we need to complete the Reload full layout through setState(). E.g:

Sugar.ifEqualBool(_countCanMod2(),    // 控制Widgte布局的
	trueValue: Image.asset('assets/image/logo.png'),
	falseValue: Image.asset('assets/image/logo2.png')),

Here we can review the overall workflow of the Fair architecture. As shown below:

image6.png

The blue nodes shown in the figure above are described as follows:

1 Dart interface source files are converted into layout DSL and logical JS files through the Fair compilation tool
2 Load the JS logic file into JSCore and complete the initialization of the JS domain
3 The DSL layout file produces the corresponding interface Widget by parsing the module
4 During the generation process of Widget, properties, events, etc. are registered; value access, etc. need to rely on the communication pipeline of 5
5 Through the communication between Flutter and the JS module, realize the value reading and method processing of the JS field

Logic processing in layout

The above section introduced the processing methods of pure logic variables and related methods, but how do we deal with the scene of mixed logic and layout?

Logic processing in layout

Fair encapsulates the logic commonly used in layout with syntactic sugar. The reason why the syntactic sugar is used is mainly to reduce the conversion cost of the Fair Compiler tool. Developers can also expand according to their own needs. An example is as follows:

condition == true ? widget1 : widget0

After encapsulation with syntactic sugar (judging whether _count is 2):

Sugar.ifEqual(_count,2, 
		trueValue:Image.asset('assets/image/logo.png'),
		falseValue: Image.asset('assets/image/logo2.png')),

In addition, the Fair layout also supports ifRang, map, mapEach and other syntactic sugars. Syntactic sugar design and implementation, see "Fair logic syntactic sugar design and implementation"

Submethod handling in layout

In a project, the layout code in the build is often very large, and most developers will split it into small layout sub-methods. E.g:

// 定义布局子方法
Widget _titleWidget() {
  return Text(getTitle());
}

// 组合布局
...
appBar: AppBar(
  title: _titleWidget(),
)
...

We transform these layout calling relationships in the DSL, and call them dynamically when the Widget is built. The specific JSON structure of the transformed DSL is as follows:

{
  "className": "Scaffold",
  "na": {
    "appBar": {
      "className": "AppBar",
      "na": {
        "title": "%(_titleWidget)"
      }
    },
    "body": {...},
  },
  "methodMap": {
    "createState": "%(_State)",
    "_titleWidget": {
      "className": "Text",
      "pa": [
        "%(getTitle)"
      ]
    }
  }
}

The sub-layout method is registered in the methodMap, and the overall assembly is completed during construction to meet the developer's needs for layout unpacking. At this point, you will also find that the design of Fair is to implement logic processing on the Dart side as much as possible, reducing the number of communications between Flutter and JS, and improving the overall dynamic performance.

Handling of Flutter type variables

At this point, students should think, has Fair implemented all the data types in Flutter on the JS side? The answer is no. Although we have prepared some corresponding types of Dart basic classes and JS basic classes on the JS side, they are not completely covered. When developers encounter Flutter's built-in types, such as ScrollController, how do we deal with it?

Fair provides a native template plus a dynamic interface mode for developers to extend. Let's take the example of loading more lists Demo, the native template is as follows:

class ListDelegate extends FairDelegate {
  ScrollController _scrollController;

  @override
  Map<String, Function> bindFunction() {
    var functions = super.bindFunction();
    functions.addAll({
      '_itemBuilder': _itemBuilder,
      '_onRefresh': _onRefresh,
    });
    return functions;
  }

  @override
  Map<String, PropertyValue> bindValue() {
    var pros = super.bindValue();

    pros.addAll({
      '_scrollController': () => _scrollController,
    });
    return pros;
  }

  @override
  void initState() {
    // 监听滑动
    _scrollController = ScrollController()
      ..addListener(() {
        //判断是否滑到底
        if (_scrollController.position.pixels ==
            _scrollController.position.maxScrollExtent) {
          _onLoadMore();
        }
      });
  }

  @override
  void dispose() {
    super.dispose();
    _scrollController.dispose();
  }

  void _onLoadMore() {
    runtime?.invokeMethod(pageName, '_onLoadMore', null);
  }

  Future<void> _onRefresh() async {
    await runtime?.invokeMethod(pageName, '_onRefresh', null);
  }
  
  // 构建item动态界面
  Widget _itemBuilder(context, index) {
    var result = runtime?.invokeMethodSync(pageName, '_onItemByIndex', [index]);
    var value = jsonDecode(result);
    var itemData = value['result'];
    return FairWidget(
      name: itemData['name'],
      path: itemData['path'],
      data: {'fairProps': jsonEncode({'item': itemData['props']})},
    );
  }
}

The advantage of this design is that when _scrollController.position monitors constant position changes, if this process is designed so that Flutter continuously sends position offset values ​​to the JS side, and the JS side implements the threshold judgment, the pressure on the bridge will be very large. Through the native template and the JS communication protocol used in the template, the logic on the JS side can be completely customized. The corresponding JS in this example is as follows:

GLOBAL['#FairKey#'] = (function (__initProps__) {
    const __global__ = this;
    return {
        list: List(), 
        _scrollController: null, 
        listIsEmpty: function listIsEmpty() {
            const __thiz__ = this;
            with (__thiz__) {
                return list == null || list.isEmpty;
            }
        }, _onLoadMore: async function onLoadMore() {
            // 上拉加载更多处理
        }, _onRefresh: async function _onRefresh() {
            // 下拉加载更多处理
        }, onLoad: function onLoad() {
            // 界面逻辑数据初始化
        }, onUnload: function onUnload() {
            // 界面逻辑数据释放
        }, _itemCount: function _itemCount() {
            // 下拉加载更多处理
        }, _onItemByIndex: function _onItemByIndex(index) {
            // 获取单个列表卡片数据
            const __thiz__ = this;
            with (__thiz__) {
                return list[index];
            }
        }
    };
})(convertObjectLiteralToSetOrMap(JSON.parse('#FairProps#')));

The method names such as onItemByIndex, onRefresh, and onLoadMore in the above code only need to correspond one-to-one with the Delegate template call on the Dart side. Complete user-defined behaviors in templates.

Finally, at the end, the students have read the design and implementation of the fair logic dynamic above, and have a certain understanding of how Fair realizes the dynamic logic of Flutter. From pure data logic, logic in layout, and finally template + view, we have fully realized the support of dynamic logic. If you have any questions or suggestions, you can leave a message directly.

The follow-up Fair application practice in the project and the industry dynamic performance data comparison will be given in the following sharing articles.

thank you all!

{{o.name}}
{{m.name}}

Guess you like

Origin http://10.200.1.11:23101/article/api/json?id=324093576&siteId=291194637