Google Flutter is an excellent cross-platform framework that not only runs on Android and iOS platforms, but also supports Web and desktop applications. Small program is a very important technology platform in China. We have been thinking about whether Flutter can be extended to small program end. Our team has opened source Alita project before, Alita can convert React Native code and run it on wechat applet platform. Inspired by this, we thought that Flutter, which is also a declarative UI framework, could also run on applets.

So we launched the Flutter_MP open source project. Take wechat mini apps for example. However, the Flutter _ MP project is still in the early experimental stage and many features are still being explored and planned. Please feel free to follow our progress on Github or join us to explore the project.

Introduction of the principle

While there’s still a lot of work to be done, let’s talk about how flutter_MP works. For reasons of length, we will briefly explain several important parts of Flutter_MP below.

Let’s take a look at flutter_mp in action:

A sample of the official Layout of Flutter

Convert through flutter_mp and run the effect on the applet side

Processing of declarative UI

Flutter is a declarative UI framework. Declarative UI only describes to the framework what the UI looks like without worrying about the implementation details of the framework. When it comes to Flutter, the upper UI description is handled by the underlying SKia graphics engine. Switching the underlying processing to HTML/CSS /canvas is flutter_web, and flutter_MP explores processing of these UI descriptions on class applets.

Let’s look at the simplest example

var x = 'Hello World'

Center(
     child: Text(x)
);
Copy the code

For the UI structure above, we just need to use the following structure in the WXML file of the applet.

// WXML Component <Center> <Text>{{x}}</Text> </Center> // js Component({data: {x: 'Hello World'}})Copy the code

Although the actual structure is much more complex than the above case, we know from the above simple example that we need to do at least two things:

We need to collect the data needed for WXML rendering according to the Flutter code generation applet WXML template file and place it in the data field of the applet component.

WXML structure generation

We know that the applets cannot operate nodes dynamically. The WXML structure needs to be pre-generated, so Flutter runs before the applets. There is a compilation and packaging phase, which iterates through the Dart code. The WXML file is generated according to certain rules (the compilation phase also does the other important thing that I’ll mention below – compiling Dart to JS).

Specifically, we first process the Dart source code into an analyzable AST structure, which is a tree representation of the source code. Then we go through the AST syntax tree structure in depth to generate the target WXML. The whole process is as follows:

The difficulty in building a WXML structure is that Flutter is not only a declarative UI but also a “value UI”. What is “value UI”? Simply put, Flutter treats UI as a normal value, like a string, like a number. Since a Flutter is a normal value, it can participate in all control processes, be the return value of a function, function parameters, and so on. While WXML for applets is a declarative UI, it is not a “value UI”. WXML is more like a template, more static. How to express dynamic “value UI” in static WXML is the key to building WXML structure.

Look at an example

Widget getX() {
    if (condition1) {
        return Text('Hello');
    } else if (condition2) {
        returnContainer( child: ... ) ; }else if (condition3) {
        returnCenter( child: ... ) ; }... } Widget x = getX(); Center(child: x // < -- how to handle x here??) ;Copy the code

Child: x x is a dynamic value that needs to be determined at run time. It can be any Widget. How do I handle this dynamic x on static WXML? Inspired by the Alita framework, this is largely due to the dynamic nature of the template applet, whose IS attribute accepts variable values. There are several steps:

  1. First, when traversing the AST structure of the Dart source code, each individual and complete “UI value” fragment is mapped to the WXML template, such as the UI in getX above
<template name="template001">
    <text>Hello</text>
</template>
<template name="template002"> <Container>... </Container> </template> <template name="template003"> <Center>... </Center> </template>Copy the code
  1. When a dynamic value such as x is encountered, a template placeholder is invariably generated
<template name="template004"> <Center> <template is="{{templateName}}" data="{{... templateData}}"/> </Center> <template name="template003">Copy the code
  1. At run time, according to getX

Function run results to determine x mapping the value of the UI, if getX condition1 inside is true, then the templateName value here is template001. Refer to the “Render Data Collection” procedure below for specific data calculation and collection. It can be seen that the way flutter_MP handles “value UI” completely refers to Alita.

Render data collection

Unlike THE WXML structure, which is generated at compile time, the render data is runtime information, which can change at any time depending on setState. So how do we collect the render data we need?

If we still follow the structure of Flutter, it will be difficult to insert the hook functions we have collected. Additionally, the structure of Flutter is too heavy for small applications. The processes shown in the red box below are not necessary for small applications to render. Finally, because the final code will be converted to JS, Flutter itself relies on many libraries that do not support js conversion, such as DART: UI, etc.

So we implemented a very minimalist version of a small application for Flutter, mini_flutter. At compile time we replaced all references to Flutter libraries with mini_flutter. The mini_flutter only exists until the Rendering stage shown above. The Rendering implementation is also customized for the widget, and the Rendering constantly collects information about Widgets during runtime. The resulting JSON structure contains the templateName and templateData described above. The UI description will be obtained by the lower applet and used to render the applet UI.

Dart/JS: Transformation and interoperation

Dart is developed in Dart and the applet runs in a browser, so Dart also needs to be compiled into JavaScript code.

Dart uses the Dart 2JS tool. However, the js code generated by Dart 2JS needs to be adapted to the applet environment. In addition, the js generated by Dart 2JS is isolated from the applet native JS environment. That is, they don’t share variables, methods, etc., they each execute in their own domain.

This raises two problems:

  1. How is the UI description JSON generated during Widget initialization or setState update passed to the applet “domain”?
  2. Related render callbacks, events that occur in the applet “domain”, how does this information get passed to Dart?

To summarize: How does Dart (which will eventually compile to JS) interoperate with applets native JS?

Dart :js and Package :js libraries are used to solve this problem:

The Dart operations JS

import 'package:js/js.dart';

@JS("JSON.stringify")
external stringify(String str);
Copy the code

So when Dart code calls the stringify method, it actually executes the window.json.stringify method

JS operation Dart

/ / register the dart
void main() {
    context['dartHi'] = () {
        print('dart hi! ');
    };
}
Copy the code
/ / js calls
window.dartHi()
Copy the code

This is just a brief illustration of Dart and JS interoperability, and the implementation of Flutter_MP is slightly different because the applet runs in a castrated browser environment.

In summary, Dart and JS are interoperable, thus opening up the upper Flutter environment and the lower applet environment.

Layout system

Flutter’s layout system is different from CSS, but very similar to it.

During Rendering, an equivalent CSS style is generated based on Widget layout attributes, categories, and constraints. Note that the boundary constraint is context-dependent. For example, the actual size of a Container with no width and height is not only related to the child elements, but also to the boundary constraints passed by the parent element. This is actually a bit troublesome. Can we express the Widget properties and boundary constraints of the Flutter completely in CSS?

conclusion

As with Flutter_web, it is not possible to completely render all features of Flutter into a small application. It is generally thought that some pages and some functions need to be run on a small application so that using Flutter_mp makes sense.

As mentioned earlier, Flutter_MP is still in its early stages, so if you need cross-application development in a production environment, we recommend using our mature RN-to-applet project, Alita.