Introduction to the

Flutter is Google’s cross-platform UI framework. It is currently the Release version 1.7. Under the background of large manpower input in mobile terminal and short-term emergency demand. Cross-end technologies will become more and more of the mobile stack of choice. The Ming Shi Tang mobile team has been experimenting and working on Flutter technology for the past few months. This article will briefly share the basic principle of Flutter and our engineering practice in e-Netcom APP.

The architecture and principles of Flutter

The architecture of the Flutter Framework layer is shown as follows:

Foundation: Foundation provides some of the base classes that the framework often uses, including but not limited to:

  • BindBase: Provides an object base class that provides singleton services, Widgets, Render, Gestures, and so on

  • Key: Provides a base class for the Key commonly used by Flutter

  • AbstractNode: represents nodes of a control tree

On top of Foundation, Flutter provides animation, drawing, gestures, rendering, and widgets, including the familiar Material and Cupertino styles

We focus on Flutter rendering from the entrance to dart

voidrunApp(Widget app) { WidgetsFlutterBinding.ensureInitialized() .. attachRootWidget(app) .. scheduleWarmUpFrame(); }Copy the code

We directly use the capabilities of the Widgets layer

widgets

Responsible for building the actual virtual node tree from the Widget tree provided by our DART code

There are three key concepts in FLutter’s rendering mechanism:

  • Widgets: Widgets that we write directly in DART that represent controls
  • Element: A virtual node that is actually built, all of which form the actual tree of controls, similar in concept to the oft-mentioned Vitrual DOM at the front end
  • RenderObject: Actually does the view work for the control. This includes layout, rendering and layer composition

According to the attachRootWidget process, we can know the layout tree construction process

  1. attachRootWidgetCreating a root node
  2. attachToRenderTreeCreate the root Element
  3. Element usingmountMethod to mount itself to the parent Element. Since it is the root node, the mount process can be ignored
  4. mountthroughcreateRenderObjectCreate the RenderObject of the root Element

At this point, the root node of the entire tree is constructed. In mount, BuildOwner#buildScope is used to create and mount child nodes, and the child’s RenderObject is attached to the parent’s RenderObejct

The whole process can be illustrated in the following diagram

See the source code for Element, RenderObjectElement, and RenderObject

Apply colours to a drawing

Responsible for the actual layout and drawing of the RenderObject, the entire control tree

The scheduleWarmUpFrame method is executed after runApp, where the rendering task is scheduled for each frame

HandleBeginFrame and handleDrawFrame go to the Binding drawFrame function, which calls WidgetsBinding and RendererBinding drawFrame in turn.

This is where Element’s BuildOwner is used to reshape our control tree.

General principle is shown in figure

When building or updating a control tree, we mark the widgets with changed parts as dirty and rebuild them, but Flutter has the judgment to reuse elements as much as possible to avoid performance problems caused by repeatedly creating Element objects.

When a dirty element is processed, it is sorted according to the element’s depth:

static int _sort(Element a, Element b) {
    if (a.depth < b.depth)
      return - 1;
    if (b.depth < a.depth)
      return 1;
    if(b.dirty && ! a.dirty)return - 1;
    if(a.dirty && ! b.dirty)return 1;
    return 0;
  }
Copy the code

The purpose of depth sorting is to ensure that the child widgets are placed to the left of the parent widgets, so that the child widgets are not repeatedly built during build time.

In the actual rendering process, Flutter uses the Relayout Boundary mechanism

void markNeedsLayout() {
    // ...
    if(_relayoutBoundary ! =this) {
      markParentNeedsLayout();
    } else {
      _needsLayout = true;
      if(owner ! =null) {
        owner._nodesNeedingLayout.add(this); owner.requestVisualUpdate(); }}/ /...
  }
Copy the code

In the control with Relayout boundary set, only the child control would be marked as needsLayout, which could ensure that after the state of the child control was refreshed, the processing scope of the control tree would be in the child tree, and the parent control would not be recreated and completely separated.

In each RendererBinding, there is a PipelineOwner object, similar to BuildOwner. BuilderOwner in WidgetsBinding, which handles the build process of the control. PipelineOwner handles the rendering of Render Tree.

@protected
  void drawFrame() {
    assert(renderView ! =null);
    pipelineOwner.flushLayout();
    pipelineOwner.flushCompositingBits();
    pipelineOwner.flushPaint();
    renderView.compositeFrame(); // this sends the bits to the GPU
    pipelineOwner.flushSemantics(); // this also sends the semantics to the OS.
  }
Copy the code

The drawFrame of the RenderBinding actually illustrates the render flow of the Render OBEJCT. Layout, Paint, compositeFrame

Scheduling (Scheduler and threading model)

In layout and rendering, we observe that the Flutter has a SchedulerBinding that provides a callback to handle frame changes. Not only does it provide scheduling for frame changes, but in SchedulerBinding, it also provides scheduling functions for tasks. This is where dart’s asynchronous task and threading model comes in.

Dart’s single-threaded model, so there is no such thing as a main thread or child thread in DART. Dart’s asynchronous operation follows the Event-Looper model.

Dart does not have the concept of threads, but does have a concept called ISOLATE. Each ISOLATE is isolated from each other and does not share memory. After the main function of the Main ISOLATE ends, events in the Event Queue are processed one by one. Dart executes synchronous code first and then asynchronous code. So if there are tasks that are very time-consuming, we can create our own ISOLATE to perform them.

Each ISOLATE has two Event queues

  • Event Queue
  • Microtask Queue

Event-looper The order in which tasks are executed is

  1. Tasks in the Microtask Queue are preferentially executed
  2. Events in the Event Queue are executed only when the Microtask Queue is empty

The asynchronous model of Flutter is shown below

Gesture

Every GUI is dependent on gesture/pointer related event handling.

In GestureBiding, in the _handlePointerEvent function, a HintTest object is created every time the PointerDownEvent event is processed. In HintTest, the path of the control node that you pass through will be stored.

And finally we’ll also see a dispatchEvent function for event distribution and a handleEvent for event processing.

In the renderView of the root node, events will start processing from hitTest, because we added the event delivery path, so time will be “processed” as it passes through each node.

@override // from HitTestDispatcher
  void dispatchEvent(PointerEvent event, HitTestResult hitTestResult) {
    if (hitTestResult == null) {
      assert(event is PointerHoverEvent || event is PointerAddedEvent || event is PointerRemovedEvent);
      try {
        pointerRouter.route(event);
      } catch (exception, stack) {
      }
      return;
    }
    for (HitTestEntry entry in hitTestResult.path) {
      try {
        entry.target.handleEvent(event, entry);
      } catch (exception, stack) {
      }
    }
  }
Copy the code

Here we can see the chronological order of the Flutter, from the root node to the child node. Similarly, when the time is processed, it passes along the child node to the parent node, eventually returning to the GestureBinding. This sequence is the same as Android’s View event distribution and browser event bubbling.

With the GestureDector Widget, you can trigger and handle a variety of such events and gestures. Refer to the Flutter documentation for details.

Material, Cupertino

Flutter is built on top of Widgets and is designed to be compatible with Android /iOS styles. Let the APP have a native-like experience on UI/UE.

Engineering practice of Flutter

Based on our own practice, I would like to share some of our insights from the perspective of mixed development, base building, and daily pit mining.

Hybrid engineering

Most of our APP themes are developed native. To practice Flutter, we need to plug Flutter into a native APP. And it can meet the following requirements:

  • This has no effect on native developers who do not participate in Flutter practice. They are not required to install the Flutter development environment
  • For those of you who are involved with FLutter, we share a dart code, a code repository

Our native architecture is multi-module componentization, where each module is a Git repository managed using the Google Git repo. Take the Android project as an example, in order to have no impact on native development. The logical idea is to provide an AAR package. From Android’s perspective, Flutter is just a FlutterView, so we can create our own module based on flutter’s engineering structure.

If we look at the build.gradle of the Andorid of the Flutter project created by Flutter Create, we can find several key points

The build of the app. Gradle

apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" flutter { source '.. /.. '}Copy the code

Here is the gradle for flutter and the source file directory for flutter.

We can guess that all builds and dependencies related to Flutter are done for us in the Gradle file for Flutter. Within the native Modules we create, we organize them in the same way. That’s it.

At the same time, we can formulate the source path of flutter according to our own reality. Repo also separates the native Module and Dart lib directories into two Git repositories. You have perfect code isolation. For native development, subsequent continuous integration such as build packaging will not be affected by Flutter.

The structure of the hybrid project is as follows:

Mixed engineering start-up and commissioning

In a flutter project, we usually start a flutter application using the flutter run command. At this time, we will pay attention to: in the hybrid project, we will enter the native page first when entering the APP, and then how to enter the flutter page. So how do we use hot overloading and debugging.

Thermal overload

Using Andorid as an example, we can print an APK package to our app using./gradlew assembleDebug.

Then use the

flutter run --use-application-binary {debug apk path}
Copy the code

Command. This will launch our native app, enter the specific flutter entry page, and the command line will automatically display the hot Reload of the flutter.

Mixed engineering commissioning

So how do we debug the Flutter project? We can map the native port to the Observatory port for mobile devices. This method can also be used when we run a pure flutter application and want to start a break in it by attaching the native process.

After the command line launches the app and the hotreload of the flutter appears, we can see that

An Observatory debugger and profiler on Android SDK built forX86 is available at: http://127.0.0.1:54946/Copy the code

This end. From this address, we can open a presentation page about dart’s performance and performance.

We record this port XXXX

And then by the adb logcat | grep Observatory checking port, you can see the following output

When we type the last address into the phone’s browser, we can see that this page can also be opened on the phone

A port mapping is performed. The port on the device is recorded as YYYY

In Android Studio, we create a Dart Remote Debug in Run -> Edit Configurations and fill in the XXXX port.

If the operation fails, you can manually forward the operation

adb forward tcp:xxxx tcp:yyyy
Copy the code

Then start the debugger and dart breakpoint debugging is ready.

Native capabilities and plug-in development

In flutter development, we often need to use native functions. For details, please refer to the official documentation. Native and Flutter can call each other by passing messages.

The architecture diagram is as follows

Looking at the source code, you can see that FLUTTER includes four Channel types.

  • BasicMessageChannelIt is a channel for sending basic information content
  • MethodChannelandOptionalMethodChannelIs the channel that sends method calls
  • EventChannelIs to send a stream of eventsstreamThe channel.

In the packaging of Flutter, the library of pure Flutter is officially defined as Package, and the Libraray invoking the native ability is defined as Plugin.

Scaffolding for the Plugin project is also provided. Create a plugin project with flutter create –org {pkgname} –template=plugin xx. It contains the library code for three ends, as well as an Example directory. Inside is a Flutter application that relies on this plugin. Refer to the plug-in documentation for details

In practice, we can find the following dependencies for the Plugin. For example, our Flutter application is called MyApp, which relies on a Plugin called MyPlugin. So, in Andorid APP, the library depends on the following figure

But if we create the plug-in project with native parts of the code, we cannot rely on the plug-in’s native AAR. This will in each time you compile GeneratedPluginRegistrant error in this class, dependence becomes a below

We will notice that the dependencies in the red dotted line do not exist in the plug-in project.

If you think about it carefully, you will find that when you use the Plugin in the Flutter application project, you simply add a Plugin dependency to pubspec.yaml. How does the native part depend on plug-ins?

By comparing the flutter create XX (application project) with the flutter create –template=plugin (plugin project), we can see some differences in settings.gradle. In the application project, there is the following automatically generated Gradle code

Gradle reads a.flutter-plugins file. Read the plugin’s native project address from it, include it, and specify the path.

Let’s look at a.flutter-plugins file:

Path_provider = / Users/chenglei/flutter /. Pub - cache/hosted/pub. The flutter - IO. Cn/path_provider - 1.1.0 /Copy the code

We can also guess that the Gradle script for Flutter relies on all the plugin projects it includes.

From this perspective, we can see that there are some regulatory limitations to plug-in project development. From a development perspective, you must code according to the specifications of the scaffolding. If you rely on other plug-ins, you must write your own scripts to resolve these dependencies. From the point of view of maintenance, the plug-in project still needs at least one Android student and one iOS student to maintain.

Therefore, we did not adopt a native engineering approach to the development of the native Flutter base library. Instead, independent Fluter package and independent Android ios Module are used as binary packages.

The path to flutter infrastructure

Based on the conclusions of the previous section, we developed our own set of flutter basics. Our infrastructure roughly starts from the following angles

  • Leverage existing capabilities: Channel based call native capabilities, such as network and log reporting. You can wrap up these basic operations in your APP
  • Quality and Stability: Flutter is new technology. How do we know when it launches
  • Development specifications: Determining the code structure and stack selection for the first release from an early stage will do more good than harm to subsequent evolutions

Leverage existing capabilities

We encapsulated the Channel and developed a DartBridge framework. Responsible for native and Dart calls to each other. On this basis, we developed network library, unified jump library and other infrastructure

DartBridge

In contrast, the communication of E Netcom APP in WebView is to call the route through a unified route call format after the message arrives at the other end. For the route provider, only the routing protocol is identified, regardless of which segment the calling end is. To a certain extent, we can also understand the unified routing protocol as “cross-platform”. The format of our internal agreement is as follows:

scheme://{"domain":"", "action":"", "params":""}

Therefore, in Flutter and native communication, combined with actual business scenarios, we do not use MethodChannel, but use BasicMessageChannel, through which we send the most basic routing protocol. Upon receiving the call, the called party invokes its own routing library and returns the call result to the channel. We encapsulated a set of DartBridges for message delivery.

Through reading the source code, we can find that the design of Channel is very perfect. It decouples the way messages are codeced, and in Codec objects we can do our own custom encodings, such as JsonMessageCodec serialized as json objects.

var _dartBridgeChannel = BasicMessageChannel(DART_BRIDGE_CHANNEL,JSONMessageCodec());
Copy the code

In real development, we might want to query message content. If the content of the message is to fetch native content, such as a student’s homework total, we want to not block our development until we serve it native. And get the mock data for the route without modifying the business code. So we added interceptors and mock services inside the route. During SDK initialization, we can configure mock data for domains and actions using object configuration.

The overall DartBridge architecture is as follows

Based on this architectural model, we receive the message and make the corresponding jump or service call through the native routing (such as ARouter) scheme.

The network library EIO

Flutter provides its own HTTP package. But when integrating into native apps, we still want the network to be a unified gateway for basic operations. This includes unified HTTPS support, unified network interception operations, and possible unified network monitoring and tuning. So in Android, the network library we chose to call OKHttp.

However, considering new business requirements, we developed a new Flutter app. We also hope that the code in the framework layer can be ported directly to the past without changing the original request.

This means the network architecture needs to decouple the network configuration from the network engine. In keeping with the principle of not reinventing the wheel, we found a great framework: DIO

DIO leaves behind an HttpClientAdapter class for network request customization.

We implement this class by calling the native network request module via DartBridge in fetch(). The returned data is a list that includes:

  • NativeBytes List Bytes stream of network data
  • StatusCode Indicates the HTTP code of the network request
  • Headers Map

    Response headers of the network
    ,>

This data is available through an Okhttp request. There is a detail here. In OkHttp, the requested bytes is a byte[], which is sent directly to dart, and I forcibly converted it into a List. Since the byte range in Java is -126-127, at this point, garbled characters appear.

By comparing the actual DART DIO request to the same byte stream, I found that some data in byte overflowed when converted to int and became negative, resulting in garbled characters. It happens to be the right complement. So. I made a unified transformation of the data on the DART side:

nativeBytes = nativeBytes.map((it) {
      if (it < 0) {
        return it + 256;
      } else {
        return it;
      }
    }).toList();
Copy the code

We will not elaborate on the encoding and decoding of UTF8 and Byte. Those of you who are interested can refer to this article

Unified route Jump

Building on the DartBridge framework, our docking native routing framework encapsulates our own unified jump. At present, our architecture is relatively simple, and we use multi-container architecture to avoid this in business. Our container page is actually a FlutterActivity. We also set a path for the container. When we jump to flutter, we actually jump to the container page. In the container page, get our actual Flutter path and parameters. The pseudocode is as follows:

valextra = intent? .extras extra? .let {val path = it.getString("flutterPath") ?: ""
            valparams = HashMap<String, String>() extra.keySet().forEach { key -> extra[key]? .let { value -> params[key] = value.toString() } } path.isNotEmpty().let {// The argument tells the first widget of flutter via bridge
                // Make a real jump within the flutter page
                DartBridge.sendMessage<Boolean> ("app"."gotoFlutter",HashMap<String,String>().apply {
                  put("path", path)
                  put("params", params)
                }, {success->
                    Log.e("Native jump flutter succeeded", success.toString())
                }, { code, msg->
                    Log.e("Native jump flutter error"."code:$code; msg:$msg")}}}Copy the code

So, do we need to know the path of the container page every time a business natively jumps to a Flutter page? Obviously not. So we abstract a list of flutter subroutes based on the above description. Perform separate maintenance. Services only need to jump to the path in its child routing table. In the SDK, the actual path is replaced by the path of the container, and the path and hop parameters of the routing table are taken as the actual parameters.

In Andorid, I provide a pretreatment function that is called in ARouter’s PretreatmentService for processing. Returns the final route path and parameters.

Quality and stability

Line switch

To ensure the stability of the new technology, a global switch configuration is provided in the Flutter base SDK. This switch is currently highly granular and controls whether to jump to the container page when entering the Flutter page. In initialization of switch processing, two parameters need to be provided

  • Whether the Flutter page is allowed to open online
  • Provide a route mapping table for the Flutter and native pages when the Flutter page cannot be opened. Jump to the corresponding native page or error page.

The online switch can be connected to the APP’s existing wireless configuration center. If there is a quality problem with the Flutter on line. We can deliver configuration to control page hopping and degrade.

Abnormal collection

In native development, we would use a tool like Bugly to look at the crash exception stack collected online. What should we do about Flutter? During the development phase, we often found that Flutter had an error page. Reading the source code, we can see that the error display is actually a Widget:

The following calls are made to the performRebuild function for ComponentElement

An ErrorWidget is returned when the build method ctach is called to an exception. On closer inspection, you can see that its Builder is a static function expression.

(FlutterErrorDetails details) => ErrorWidget(details.exception)

Its arguments also eventually return a private function expression _debugReportException

Finally, the onError function is called, which is also a static function expression

So for exception catching, we only need to override the following two functions to report the view error in the build method

  • ErrorWidget.builder
ErrorWidget.builder = (details) {
  return YourErrorWidget();
};
Copy the code
  • FlutterError.onError
FlutterError.onError = (FlutterErrorDetails details) {
  // your log report
};
Copy the code

At this point, we’ve done exception catching for the view. How do you catch exceptions thrown in dart’s asynchronous operation? After consulting the data, we came to the following conclusions:

Within Flutter there is the concept of a Zone, which represents an independent environment for the asynchronous operation of the current code. Zones can capture, intercept, or modify some code behavior

Finally, our exception collection code looks like this


void main() {
  runMyApp();
}

runMyApp() {
  ErrorHandler.flutterErrorInit();  // Set synchronized exception handling requirements
  runZoned(() => runApp(MyApp()), // Execute MyApp in zone
      zoneSpecification: null,
      onError: (Object obj, StackTrace stack) {
        // Unified exception catching in Zone
    ErrorHandler.reportError(obj, stack);
  });
}
Copy the code

The development of specification

At the beginning of development, we agreed internally on our specifications for Flutter development. The focus is on the code’s organizational structure and state management libraries. Considering the possibility of adding most of the Flutter code in the future, we chose to manage their directories by business modules.

.
+-- lib
|   +-- main.dart
|   +-- README.md
|   +-- business
|       +-- business1
|           +-- module1
|               +-- business1.dart
|               +-- store
|               +-- models
|               +-- pages
|               +-- widgets
|               +-- repositories
|               +-- common
|                   +-- ui
|                   +-- utils
|   +--comlib
|       +-- router
|       +-- network
Copy the code

Within each business, the concepts of Page and widgets are divided into pages and specific view modules. In store, we store the associated state management. In our case, we require the business to abstract their respective logical and purely asynchronous operations into a separate layer. Each business can maintain its own common early on, continuously abstract its own Pakcage in iterations, and precipitate it into the final comlib for everyone. In this way, it is almost guaranteed to avoid the code redundancy and confusion caused by people repeating the wheel during iteration.

In the selection of state management techniques, we investigated Bloc, ‘redux ‘and’ MOBx ‘. Our conclusion is that

  • flutter-reduxThe concept and design are excellent, but suitable for unified global state management, in fact, and component segmentation is a big contradiction. In open source solutions, we find thatfish-reduxThat’s a good solution to the problem.
  • BlocThe general idea of Redux is very similar to redux. But it still doesn’t have as much functionality as Redux.
  • mobx, the code is simple, fast. Basically figure it outObservables,ActionsandReactionsA few concepts can be developed happily.

In the end, we chose MOBX as our state management component because of the cost of getting started and the complexity of the code.

conclusion

Here I share some of the principles of Flutter and some of our practices. I hope I can communicate and learn from some students who are studying Flutter. Our Flutter infrastructure was developed along with some of the pages on the upschool E-Netcom APP and some of the basic UI component libraries. In the future we will try to launch the Flutter version in some old pages. We also look at better base libraries, exception collection platforms, toolchain optimization, and single-container related content.