Fair 1.0, we made the layout dynamic, but the data part was not dynamic. In response to this problem, we launched 2.0 support for logic dynamics. Fair logic dynamic, we basically realize the data related logic processing capabilities (logic here is not only logical operations, including operations, branches, loops and other logical processing capabilities). Let’s take a closer look at where logical expressions occur and how they can handle different data types.

The core contents of this paper include:

Data logic processing

Logical processing in layout

Flutter type data processing

Data logic processing

Every Flutter interface we touch consists mostly of code related to layout and logic. (We focus on the interface of the StatefulWidget.)

Class _MyHomePageState extends State<MyHomePage> {// variable int _counter = 0; Void _incrementCounter() {setState(() {_counter++; }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: The Text (widget. The title), / / external parameters), the body: Center (child: the Column (mainAxisAlignment: mainAxisAlignment Center, the children: <Widget>[Text('You have pushed the button this many times:',), Text('$_counter', // variable using style: Theme.of(context).textTheme.headline4, ), ], ), ), floatingActionButton: FloatingActionButton( onPressed: _incrementCounter, // Event callback Tooltip: 'Increment', Child: Icon(icons.add),),); }}Copy the code

As above, annotations mark the data and the logic of the data processing on the page. The logic dynamics of Fair is to implement the scripting dynamic parsing of variables, methods and other contents, as well as the binding of DSL layout properties and subsequent user action event method invocation.

How do I turn the logic in the Dart file into dynamically runnable variables and associated operations? At the beginning of the project, we also discussed several schemes and tried Demo development. For example, directly delivering the Dart file to complete layout and logic parsing and data binding on the pure Dart side is similar to developing a Dart Script engine, which requires high development costs. We finally decided to develop Dart2JS tool, convert the logical part of Dart file on the interface into JS script, and then use the native JSCore to complete the dynamic running of the logical script. Thus, our final structure is as follows:

We implemented a Widget annotated with @FairPatch annotations and compiled it with the Fair-Compiler tool to generate DSL layout files and JS logic files. Why do we need to use FairPatch annotation, which is generated after annotation? This is because our overall design concept is that the native and dynamic interfaces of Flutter can be converted using A single source file. Dynamic is only used for temporary scenarios such as urgent requirements or A/B tests with uncertain requirements. After the requirements are stable, the generated source file can be used with the release to maximize the performance of Flutter.

The dynamic script converted from the Widget is shown in the figure below. See “Fair Deliverables –DSL Generation Principle and Implementation” and “Fair Deliverables –Dart2JS Generation Principle and Implementation” for details.

The original Dart syntax that we need to translate has increased considerably during product generation in order to support logic dynamics. I’ll take the _counter variable from the Demo example, which is a basic int. Used in the following code:

Text('$_counter')
Copy the code

Although it may seem like a short statement, the data binding phases need to be carefully distinguished when generating a Text Widget. Num = string;

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

Logic dynamic, the core of the difficulty is how to deal with variables. Variables in Flutter can be data variables or method variables. According to the scenario in the Demo, I enumerate (we merge variable expressions in the same way according to the need of data binding) :

String strong to base variable

Text('$_counter') => #($_counter)
Copy the code

Widget pass parameter variables

Text(widget.title) => #(widget.title)
Copy the code

Method variables (callback execution)

onPressed: _incrementCounter => @(incrementCounter)
Copy the code

The variables and logical methods in the Flutter code are translated by the Fair compilation tool into the identifiers in the DSL file. 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 process of data binding is to complete the variable processing requirements of Flutter and JS according to the expressions #() and @() mentioned above, and finally to complete the generation of the target Widget.

Dart and JS two-domain variable mapping

Fair framework design: Only from the Dart side to initiate the required variable value query and variable processing method calls, JS side basically only as data value storage and data logic related operations.

The MVVM in Fair relies on the native mode of Flutter, as shown above. Data from the JS domain is synchronized to the Dart domain by invoking the familiar setState on the JS side. Of course, this part is not sensitive to developers using the Fair framework, but the compilation tool does the conversion for us. Native code and generated JS code are compared as follows:

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

As you can see, there isn’t much difference in the code, except for JS’s with, which simplifies access to the domain, and fairkeys, which are required to communicate with target objects. For a detailed communication design, see Fair Logical Dynamic Communication Implementation.

Life cycle:

I’ve shown you how to map the values of variables in two fields. Some of you might be wondering, how do Dart and JS life cycles synchronize? (Fair provides the StatefulWidget carrier, so only the relationship between the StatefulWidget lifecycle and the JS lifecycle is described here.)

As shown in the figure above, Fair provides two aware lifecycle callbacks to the JS domain. OnLoad is to inform JS side of variable initialization after JS loading is completed; OnUnload Means that the page disappears and notifies the JS side to reclaim resources.

Minimum render:

In Fair, ValueNotifier objects are generated for each value variable of the Widget property to implement local component rendering. Computational variables, such as method parameters, are not treated in a special way. For variables that participate in Widget hierarchy management, we need setState() to complete the Reload layout. Such as:

IfEqualBool (_countCanMod2(), // trueValue: image.asset ('assets/ Image /logo.png'), falseValue: Image.asset('assets/image/logo2.png')),Copy the code

Here we can review the workflow of the Fair architecture as a whole. As shown below:

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

Dart interface source files are converted into layout DSL and logical JS files using the Fair compilation tool

The JS logical file is loaded into JSCore and the JS field is initialized

3 DSL layout file generates corresponding interface widgets by parsing modules

4 During Widget generation, properties and events are registered; Value access and so on depend on the 5 communication channel

5 Through the communication between Flutter and JS module, the value reading and method processing of JS domain can be realized

Logical processing in layout

The previous section explained how to deal with purely logical variables and related methods, but what about scenarios where logic and layout are mixed?

Logical processing in layout

Fair encapsulates syntactic sugar in logic that is often used in layouts. The main reason for using syntactic sugar is to reduce the conversion cost of the Fair Compiler tool. Developers can also extend as they see fit. Examples are as follows:

condition == true ? widget1 : widget0
Copy the code

With syntactic sugar encapsulation (check if _count is 2) :

IfEqual (_count, 2, trueValue:Image. Asset ('assets/ Image /logo.png'), falseValue: Image.asset('assets/image/logo2.png')),Copy the code

The Fair layout also supports syntax sugars such as ifRang, Map, and mapEach. Syntax sugar design and implementation, see “Fair Logic Syntax sugar Design and Implementation”

Submethod processing in the layout

In a project, the layout code in the build tends to be so large that most developers break it down into small layout submethods. Such as:

// Define layout submethod widget-_titleWidget () {return Text(getTitle()); } // Combine layout... appBar: AppBar( title: _titleWidget(), ) ...Copy the code

We transform these layout call relationships in the DSL and call them dynamically during Widget build. The specific transformation DSL JSON structure is as follows:

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

The sub-layout methods are registered in methodMap and the whole is assembled at build time to meet the developer’s need to unpack the layout. As you can see, Fair is designed to minimize the logical processing of Flutter and JS on the Dart side to improve the overall dynamic performance.

Processing of Flutter type variables

At this point you might be wondering, has Fair implemented all the data types in Flutter on the JS side? The answer is no. On the JS side, although we have prepared some corresponding types of Dart base classes and JS base classes, they are not completely covered. What do we do when developers encounter Flutter built-in types, such as ScrollController?

Fair provides a native template plus dynamic interface model for developers to extend. We used to load more list Demo examples, 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() {// Listen to slide _scrollController = ScrollController().. AddListener () {/ / determine whether slippery the if (_scrollController. Position. The 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); } // Build 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']})}, ); }}Copy the code

The benefits of this design, there are like _scrollController. The location of the position to monitor constantly change, if this process is designed to Flutter sent to JS side position offset value, the realization of the threshold by JS side, on the bridge, pressure will be very big. With the native template and the JS communication protocol used in the template, the logic of 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() {// Pull up load more processing}, _onRefresh: Async function _onRefresh() {onLoad: function onLoad() {// interface logical data initialization}, onUnload: Function onUnload() {_itemCount: function _itemCount() {_onItemByIndex: Function _onItemByIndex(index) {const __thiz__ = this; with (__thiz__) { return list[index]; }}}; })(convertObjectLiteralToSetOrMap(JSON.parse('#FairProps#')));Copy the code

The onItemByIndex, onRefresh, onLoadMore method names in the code above just need to correspond to the Delegate template calls on the Dart side. Complete custom behavior in templates to support users.

Finally, after reading the design and implementation of Fair logic motility, students had a certain understanding of how Fair realized the motility of Flutter logic. We fully implement logic dynamic support from pure data logic, logic in layout, and finally template + view. If you have any questions or suggestions, please leave a message directly.

A comparison of Fair’s application practices and industry dynamic performance data will be provided in a later sharing article.

Thank you!