Airing

Airing

哲学系学生 / 小学教师 / 程序员,个人网站: ursb.me
github
email
zhihu
medium
tg_channel
twitter_id

Flutter Core Principles and Hybrid Development Model

In the practice of Flutter hybrid development for "Fan Live," we have summarized some experiences in Flutter hybrid development to share. This article will start from the principles of Flutter, detailing the drawing principles of Flutter, and using this to compare three cross-platform solutions; then we will move on to the third chapter explaining the Flutter hybrid development model, mainly analyzing the principles of four different Flutter hybrid modes; finally, we will briefly share the exploration of engineering in hybrid projects.

"Only by going deep can one explain it simply." For a technology, only by understanding it deeply can one describe it in the simplest and most straightforward terms. Before this, I had written some articles on Flutter, but they were more like learning notes and source code reading notes, thus rather obscure and fragmented. As a summary of this stage, I will try to share knowledge about Flutter hybrid development in an easy-to-understand manner, progressing step by step. Key content will be supplemented with source code or key functions from the source code for interpretation, but I will not paste large sections of source code. The effectiveness of learning from source code mainly depends on oneself, so if you are interested in learning from the source code, you can read the source code of the Framework and Engine, or refer to my previous articles.

Alright, without further ado, let's get started!

1. Core Principles of Flutter#

1.1 Flutter Architecture#

flutter architecture

Note: This image is sourced from Flutter System Overview

Traditionally, whenever an article discusses the principles of Flutter, this image is always presented at the beginning. Regardless of how well it is explained, it is first laid out, and most of the understanding still relies on personal insight. This image is indeed very useful.

By presenting this image, we can first get a simple understanding of what Flutter is, otherwise, it is easy to fall into the situation of "blind men touching an elephant."

The Flutter architecture adopts a layered design, divided into three layers from bottom to top: Embedder, Engine, Framework.

  1. Embedder: The operating system adaptation layer, implementing rendering surface settings, thread settings, etc.
  2. Engine: Implements Flutter rendering engine, text layout, event handling, Dart runtime, etc. It includes the Skia graphics library, Dart VM, Text, etc., where Skia and Text provide the ability to call underlying rendering and layout for the upper layer interfaces.
  3. Framework: A UI SDK implemented in Dart, which includes two major style component libraries, basic component libraries, graphics rendering, gesture recognition, animations, etc.

For more details, this image combined with source code will provide a better experience. However, since this article is not a source code analysis, we will not elaborate on that here. Next, I will use the Flutter drawing process as an example to explain how Flutter works. This will also help you better understand the thinking behind the source code.

1.2 Flutter Drawing Principles#

The Flutter drawing process can be summarized as shown in the following image:

flutter-render.png

First, user actions trigger updates to the Widget Tree, then the Element Tree is constructed, the repaint area is calculated, and the information is synchronized to the RenderObject Tree, followed by component layout, component drawing, layer composition, and engine rendering.

As a prerequisite, let's first look at the data structures involved in the rendering process, and then analyze each specific step of the rendering.

1.3 Data Structures in the Flutter Rendering Process#

Data Model

The key data structures involved in the rendering process include three trees and one layer, where RenderObject holds the Layer. We will first focus on the relationships between the three trees.

For example, consider a simple layout:

Layout

The relationships between the corresponding three trees are shown in the following image:

Three Trees

1.3.1 Widget Tree#

Widget Tree

The first tree is the Widget Tree. It is the basic logical unit for implementing controls and is the way users describe the UI.

It is important to note that Widgets are immutable. When the view configuration information changes, Flutter will rebuild the Widget to update it, constructing a simple and efficient UI in a data-driven manner.

So why is the Widget Tree designed to be immutable? Flutter UI development is a form of reactive programming that advocates "simple is fast." Recreating the Widget Tree from top to bottom for refresh is a straightforward approach that does not require additional relationships to determine which nodes are affected by data changes. Additionally, a Widget is merely a configuration data structure; its creation is lightweight, and destruction has been optimized, so there is no need to worry about performance issues arising from reconstructing the entire tree.

1.3.2 Element Tree#

Element Tree

The second tree is the Element Tree. It is the instantiated object of the Widget (as shown in the image, the Widget provides the createElement factory method to create an Element), which persists in the runtime Dart context. It carries the context data for building and serves as the bridge connecting structured configuration information to the final rendering.

The reason it persists in the Dart context rather than being rebuilt like the Widget is that the overhead of recreating and re-rendering the Element Tree is very high. Therefore, there is a Diff stage from the Element Tree to the RenderObject Tree to calculate the minimal repaint area.

code

It is important to note that the Element holds both the Widget and the RenderObject, but neither the Widget nor the Element is responsible for the final rendering; they merely "issue commands." The actual rendering of the configuration information is done by the RenderObject.

1.3.3 RenderObject Tree#

RenderObject Tree

The third tree is the RenderObject Tree, which is the rendering object tree. RenderObject is created by the Element and is associated with Element.renderObject (as shown in the image). It synchronizes information from the Element and also persists in the Dart Runtime context, being the main object responsible for implementing view rendering.

code

The RenderObject Tree in Flutter's display process is divided into four stages:

  1. Layout
  2. Painting
  3. Composition
  4. Rendering

Among them, layout and painting are completed in the RenderObject. Flutter uses a depth-first mechanism to traverse the rendering object tree, determining the position and size of each object in the tree and drawing them onto different layers. After drawing is complete, the composition and rendering tasks are handed over to Skia.

So the question arises, why are there three trees instead of two? Why is an intermediate Element Tree needed? Can't the Widget Tree directly construct the RenderObject Tree?

Theoretically, it is possible, but practically it is not feasible. Directly constructing the RenderObject Tree would greatly increase the performance overhead of rendering. The Widget Tree is immutable, but the Element is mutable. In fact, the Element layer abstracts the changes in the Widget tree (similar to React/Vue's VDOM Diff), synchronizing only the parts that truly need modification to the RenderObject Tree, thereby minimizing the repaint area and improving rendering efficiency. It can be seen that Flutter's philosophy largely draws from the front-end reactive frameworks React/Vue.

Comparison

Additionally, let's expand on VDOM. We know that the advantages of Virtual DOM include:

  1. Diff algorithm, ensuring minimal DOM node operations. This is reflected in Flutter's Element Tree.
  2. Declarative UI programming, strong code maintainability. This is evident when writing UI components declaratively in Dart.
  3. Abstracting real nodes facilitates cross-platform implementation. This is not reflected on the Flutter side, as Flutter itself is a cross-platform self-drawing engine. However, from another perspective, could we construct the Widget Tree of Elements without using Dart, but instead using another language that supports runtime compilation (like JavaScript)? Wouldn't that allow for dynamic implementation? Yes, currently MXFlutter is achieving dynamic implementation with this approach.

1.3.4 Layers#

layers

Finally, let's look at Layers, which are attached to RenderObject (accessed via RenderObject.layer) and serve as carriers for drawing operations, as well as caching the results of drawing operations. Flutter draws on different layers and then overlays these layers with cached drawing results according to rules to obtain the final rendering result, which is what we refer to as the image.

code

As shown in the code above, the Layer base class has two properties _needsAddToScene and _subtreeNeedsAddToScene, where the former indicates whether it needs to be added to the scene, and the latter indicates whether the subtree needs to be added to the scene. Typically, only when the state has been updated does it need to be added to the scene, so these two properties can be intuitively understood as "needs to update itself" and "subtree needs to update."

Layer provides markNeedsAddToScene() to mark itself as "needs to update." Derived classes call this method to mark themselves as "needs to update" when their state changes, such as when child nodes are added or removed in ContainerLayer, changes in opacity in OpacityLayer, or changes in the picture in PictureLayer, etc.

1.4 Breakdown of Flutter Drawing Process#

The drawing process is divided into the following six stages:

  1. Build
  2. Diff
  3. Layout
  4. Paint
  5. Composite
  6. Render

Leaving aside Diff and Render, which we will not explain in this article as they are somewhat more complicated, let's focus on the remaining four stages.

Drawing Process

Note: This flowchart is sourced from How to Ensure High Performance and Smoothness of Flutter in Complex Business? | Xianyu Technology, which clearly expresses the core drawing process of Flutter.

1.4.1 Build#

When executing the build method, there are two different logics based on the type of component.

We know that Widgets in Flutter can be divided into StatelessWidget and StatefulWidget, i.e., stateless and stateful components.

A StatelessWidget is one whose build information is entirely composed of configuration parameters (input parameters); in other words, once created, it no longer cares about or responds to any data changes for redrawing.

StatelessWidget

A StatefulWidget, in addition to the static configuration passed in during the parent component's initialization, also needs to handle user interactions and internal data changes (such as network data responses) and reflect these in the UI. This type of component needs to implement the design of building the Widget using a State class. It constructs the UI using the build method of State, ultimately calling the buildScope method, which traverses _dirtyElements and calls rebuild/build on them.

StatefulWidget

Note: The above two images are sourced from “Core Technologies and Practice of Flutter | Chen Hang”

1.4.2 Layout#

Only layout-type Widgets will trigger layout (such as Container, Padding, Align, etc.).

Each RenderObject node needs to do two things:

  1. Call its own performLayout to calculate the layout.
  2. Call the child's layout and pass in the parent's constraints.

code

This recursive round ensures that each node is constrained by its parent and calculates its own size, allowing the parent node to determine the positions of its child nodes according to its own logic, thus completing the entire Layout stage.

layout

1.4.3 Paint#

In the rendering pipeline, the first step is to identify the RenderObject that needs to be redrawn. If it implements CustomPainter, the CustomPainter paint method is called, followed by the child's paint method; if CustomPainter is not implemented, the child's paint is called directly.

When calling paint, after a series of transformations, layer->PaintingContext->Canvas, the final paint is drawn on the Canvas.

code

1.4.4 Composite#

Composition mainly does three things:

  1. Combines all Layers into a Scene.
  2. Submits the Scene to the Engine via the ui.window.render method.
  3. The Engine calculates the final display effect of all Layers and renders it to the screen.

code

2. Comparison of Cross-Platform Solutions#

Cross-platform development is an inevitable trend. Essentially, it increases the reuse rate of business code, reduces the workload caused by adapting to different platforms, and thus lowers development costs. Before the differences between platforms are smoothed out, if one wants to develop programs that provide a similar experience across platforms "quickly, well, and economically," cross-platform development is the way to go.

In general, the industry widely recognizes the existence of three types of cross-platform solutions:

  1. Web container solutions
  2. Generic Web container solutions
  3. Self-drawing engine solutions

Let's explain each one.

2.1 Web Container#

Cross-Platform Solution 1

A Web container refers to implementing interfaces and functionalities based on web-related technologies through browser components, including what we commonly refer to as WebView-based "H5," Cordova, Ionic, and WeChat mini-programs.

This type of hybrid development model only requires developing once for the web, allowing it to run in the browser components of multiple systems, maintaining a generally consistent experience. It is currently a very popular cross-platform development model. Communication between the web and the native system is accomplished through JSBridge, where the native system exposes capabilities to be called by the web via the JSBridge interface. The presentation of the page is handled by the browser component, which loads, parses, and renders the web according to standard browser rendering processes.

The advantages of this solution are: simplicity, inherent support for hot updates, a thriving ecosystem, strong compatibility, and a friendly development experience.

Of course, the drawbacks are also quite evident; otherwise, the latter two solutions would not exist. The main issues are related to user experience:

  1. The browser rendering process is complex, and pages need to be loaded online, limiting the experience due to network constraints. Therefore, the web has white screen time (except for PWA), and the interactive experience is very, very different from the native experience.
  2. Both ends need to implement JSBridge interfaces separately, and the communication efficiency of JSBridge is generally low.

2.2 Generic Web Container#

Cross-Platform Solution 2

Thus, the generic Web container solution comes into play, with representative frameworks being React Native, Weex, and Hippy.

  • It abandons browser rendering and adopts native controls to ensure interaction experience;
  • It supports built-in offline packages to avoid loading delays and long white screens;
  • It still uses the front-end friendly JavaScript language to ensure a good development experience.

In terms of cross-platform communication, React Native still uses the Bridge method to call methods provided by the native side.

This solution is ideal in theory, but the reality is quite stark, as it has also encountered problems in practice:

  1. Directly calling native controls improves experience and performance, but the rendering results of the same native controls differ across platforms, requiring a huge amount of work to smooth out cross-platform differences.
  2. The efficiency of Bridge communication is generally low, which can lead to frame drops in scenarios requiring high-frequency communication.

2.3 Self-Drawing Engine#

So can we simply smooth out differences while ensuring performance?

The answer is yes, and that is the self-drawing engine. By not calling native controls, we draw ourselves. That is Flutter. For example, if a police officer asks React Native what the suspect looks like, React Native can only vividly describe the suspect's appearance, and after the police draw it, React Native has to confirm whether it looks like the suspect; but Flutter is like a sketch master, capable of drawing the suspect's portrait itself and presenting it to the police. The differences in efficiency and performance between the two are self-evident.

  1. It directly calls OpenGL rendering through the Skia graphics library, ensuring high-performance rendering while smoothing out differences.
  2. The choice of development language supports both JIT and AOT in Dart, ensuring development efficiency while significantly improving execution efficiency compared to JavaScript.

Cross-Platform Solution 3

With this approach, Flutter can minimize differences between platforms while maintaining high performance similar to native development. Additionally, for system capabilities, Flutter projects can support reuse through the development of Plugins. Therefore, Flutter has become the most flexible among the three types of cross-platform solutions and is currently a framework of significant interest in the industry.

As for communication efficiency, Flutter's cross-platform communication efficiency is also much higher than that of JSBridge. Flutter communicates through Channels, including:

  1. BasicMessageChannel, used for passing strings and semi-structured information, is full-duplex and can request data bidirectionally.
  2. MethodChannel, used for passing method calls, allowing Dart to call methods on the native side and callback results through the Result interface.
  3. EventChannel: User data stream communication, allowing Dart to listen for real-time messages from the native side, immediately callback to Dart when the native side generates data.

Among these, MethodChannel is used more frequently in development. The following image shows a standard MethodChannel call principle diagram:

MethodChannel

But why do we say that the performance of Channels is high? Let's outline the call stack during a MethodChannel call, as shown in the following image:

Communication Efficiency

It can be seen that the entire process involves the transfer of machine code, and the communication efficiency of JNI is similar to that of internal communication within the JavaVM. The entire communication process is equivalent to internal communication on the native side. However, there are bottlenecks. We can see that methodCall requires encoding and decoding, and the main consumption is in encoding and decoding, so MethodChannel is not suitable for passing large-scale data.

For example, if we want to call the camera to take photos or record videos, but during the process of taking photos and recording videos, we need to display the preview on our Flutter UI. If we use MethodChannel to implement this functionality, we would need to pass every frame of the image captured by the camera from the native side to the Dart side, which would be very costly, as transmitting image or video data in real-time through message channels would inevitably lead to huge memory and CPU consumption. Therefore, Flutter provides a texture-based image data sharing mechanism.

The discussion of Texture and PlatformView is beyond the scope of this article, so we will not delve deeper here. Interested readers can refer to relevant materials for further knowledge.

Next, let's move on to the third chapter of this article, the exploration of Flutter hybrid development modes.

3. Flutter Hybrid Development Modes#

3.1 Hybrid Modes#

The structure of Flutter hybrid projects mainly consists of the following two modes:

  1. Unified management mode
  2. Three-end separation mode

Two Hybrid Modes

The so-called unified management mode is a standard Flutter Application project, where the Flutter product project directories (ios/ and android/) can be mixed with native projects, similar to how React Native performs hybrid development within project structures. However, the drawback of this approach is that when the native project becomes large, the coupling of the Flutter project to the native project becomes very severe, making upgrades cumbersome. Therefore, this hybrid mode is only suitable for projects where Flutter is the main business and native functionality is supplementary. However, in the early days when Google did not support Flutter Module, this was the only mode for hybrid development.

Later, Google provided better support for hybrid development. In addition to Flutter Application, it also supports Flutter Module. As the name suggests, a Flutter Module supports modular integration of Flutter into native projects, with its products being Frameworks or Pods on iOS and AARs on Android. The native project can introduce the Flutter Module using Maven and Cocoapods like any other third-party SDK. This achieves a truly separated development mode across three ends.

3.2 Hybrid Stack Principles#

Hybrid Stack

For simplicity, we will not consider the unification of lifecycles and the implementation of communication layers here. Apart from that, the hybrid navigation stack mainly needs to solve problems in the following four scenarios:

  1. Native to Flutter
  2. Flutter to Flutter
  3. Flutter to Native
  4. Native to Native

3.2.1 Native to Flutter#

Native -> Flutter, this case is relatively simple. The Flutter Engine has already provided us with ready-made Plugins, namely FlutterViewController on iOS and FlutterView on Android (which can be wrapped to implement FlutterActivity). Therefore, in this scenario, we can directly use the initialized Flutter Engine to set up the Flutter container, set its initial route page, and then jump to the Flutter page in a native way.

code

3.2.2 Flutter to Flutter#

Flutter -> Flutter, there are two solutions in the industry, which we will detail later:

  1. Using Flutter's own Navigator navigation stack
  2. Creating a new Flutter container and using the native navigation stack

3.2.3 Flutter to Native#

Flutter -> Native, it is important to note that this jump actually includes two situations: one is to open a native page (open, including but not limited to push), and the other is to return to a native page (close, including but not limited to pop).

flutter->native

As shown in the image, this situation is relatively complex. We need to use MethodChannel to enable communication between Dart and the Platform side. After Dart issues open or close commands, the native side executes the corresponding logic.

3.2.4 Native to Native#

Native -> Native, there is nothing much to say about this situation; just use the native navigation stack directly.

3.3 Hybrid Modes#

To address the hybrid stack issues and to compensate for Flutter's own lack of support for hybrid development, several hybrid stack frameworks have been proposed in the industry. Overall, they can be categorized into four hybrid modes:

  1. Flutter Boost, representing a WebView-like navigation stack
  2. Flutter Thrio, representing a Navigator navigation stack
  3. Multi-Engine hybrid mode
  4. View-based hybrid mode

Let's discuss their principles and pros and cons one by one.

3.3.1 Flutter Boost#

Flutter Boost is an open-source Flutter hybrid framework developed by the Xianyu team. It is mature and stable, with high industry influence. Its handling of the navigation stack does not deviate from the hybrid stack principles discussed in section 3.2. However, it is important to note that when Flutter jumps to another Flutter page, it creates a new FlutterViewController and uses the native navigation stack to perform the jump, as shown in the image below:

flutter boost

The advantage of this approach is that users (business developers) operate the Flutter container just like they would a WebView, and Flutter pages behave like web pages, making the logic simple and clear, consolidating all navigation and routing logic to be handled by the native side. The following image shows the sequence diagram (key function path) when calling the open method in Flutter Boost, where two points of information can be observed:

  1. The logic of the hybrid navigation stack mainly includes the native layer, communication layer, and Dart layer.
  2. The implementation logic of Flutter Boost's open method is relatively simple.

flutter boost open sequence diagram

However, it also has a drawback: each time a Flutter page is opened, a new ViewController needs to be created, resulting in additional memory overhead in scenarios with consecutive Flutter to Flutter jumps. To address this issue, another team developed Flutter Thrio.

3.3.2 Flutter Thrio#

As mentioned earlier, Flutter Boost has additional memory overhead in scenarios where Flutter jumps to Flutter pages. Therefore, the Hello Chuxing team open-sourced the Flutter Thrio hybrid framework in April this year. The most significant change it made compared to Flutter Boost is that in scenarios where Flutter jumps to Flutter, Thrio uses the Flutter Navigator navigation stack. As shown in the image below:

flutter thrio

In scenarios with consecutive Flutter page jumps, the memory test chart is as follows:

Memory Increment

From this chart, we can derive the following points of information:

  1. The red area represents the memory increment from starting the Flutter Engine, which is close to 30MB; the Flutter Engine is a relatively heavy object.
  2. The memory increment brought by FlutterViewController generally ranges from 12 to 15MB.

It can be seen that in this scenario, Thrio has indeed made some optimizations. However, this comes at the cost of increased implementation complexity. We mentioned that Flutter Boost's advantage is its simplicity, with all routing handled by the native navigation stack. In contrast, Flutter Thrio mixes the native navigation stack and Flutter Navigator, making the implementation relatively more complex. Here, I have outlined the key function path for Flutter Thrio's open method, which shows that Thrio's navigation management is indeed more complex.

thrio sequence diagram

3.3.3 Multi-Engine Mode#

The two hybrid frameworks we discussed above are single-engine frameworks, and there are also multi-engine frameworks. Before discussing multi-engine, we need to introduce some prerequisites regarding Engine, Dart VM, and isolate.

In the first chapter, we did not cover the source code analysis of the Engine layer, focusing instead on explaining the principles of the Framework layer. This was done for the sake of coherence in the first chapter, and it is best to explain Engine here for better memory and understanding.

Dart VM, Engine, and Isolate#

(a) After the Dart virtual machine is created, it needs to create an Engine object, which will then call DartIsolate::CreateRootIsolate() to create an isolate.
(b) Each Engine instance creates its own new Thread for UI, GPU, IO, and Platform Runner.
(c) Isolate, as the name suggests, has logically isolated memory.
(d) The code within an isolate is executed sequentially, and any concurrency in Dart programs results from running multiple isolates. Of course, we can start multiple isolates to handle CPU-intensive tasks.

From (a), we can conclude: (1) Each Engine corresponds to a Root Isolate object.
From (b), we can conclude: (2) Engine is a relatively heavy object (as mentioned earlier).
From (c) and (1), we can conclude: (3) Engines are isolated from one another.
From (d) and (3), we can conclude: (4) Engines do not share memory for concurrency, eliminating the possibility of competition, and thus there is no need for locks, which means no deadlock issues.

Now that we have remembered these four conclusions, let's look at the window.

Window#

The window is the drawing window and serves as the connection between the Flutter Framework (Dart) and the Flutter Engine (C++).

From the class definition, the window is the interface connecting the Framework and Engine. In the Framework layer, the window refers to the ui.window singleton object, with the source file being window.dart. In the Engine layer, the source file is window.cc, and the APIs for interaction between the two are minimal but correspond one-to-one:

code

code

It can be seen that these mainly involve the Framework layer calling the relevant APIs encapsulated from the Skia library in the Engine layer. This leads us to its second layer of meaning—serving as a drawing window.

Functionally, in terms of interface drawing interaction, the window is also the drawing window. In the Engine, drawing operations output to a PictureRecorder object; on this object, calling endRecording() yields a Picture object, which needs to be added to a SceneBuilder object at the appropriate time; calling the build() method on the SceneBuilder object obtains a Scene object; finally, at the right moment, the Scene object is passed to the window.render() method, rendering the scene.

window

This image is sourced from: Flutter Framework Source Code Analysis (1) — Introduction and Usage of the Drawing Engine

The example code is as follows:

code

Multi-Engine Mode#

Based on the above, we can derive the following diagram for the multi-engine mode:

Multi Engine Mode

It has the following characteristics:

  1. The app contains multiple engines.
  2. Each engine has several FlutterVCs.
  3. Engines are isolated from one another.

Based on these three characteristics, we can imagine the implementation of the communication layer. Suppose there are two engines, each containing two FlutterVCs, and each FlutterVC contains two Flutter pages. The navigation in such a scenario would become very complex (the following image is sourced from the README of the Thrio open-source repository):

Multi Engine Mode

It is evident that while the logical isolation between Engines brings natural isolation between modules, it also presents many issues.

First, as shown in the image, the design of the communication layer will be exceptionally complex, and the core logic of the communication layer still needs to be implemented on the native side, which somewhat undermines the advantages of cross-platform development.

Secondly, we have repeatedly mentioned that the Engine is a relatively heavy object, and starting multiple Flutter Engines will lead to excessive resource consumption.

Finally, since Engines do not share memory, this natural isolation actually causes more harm than good. From the perspective of hybrid development, an app needs to maintain two sets of cache pools—one for the native cache pool and one for the cache pool held by the DartVM. However, with the introduction of multiple Engines, the latter cache pool's resources are not interconnected, leading to even greater resource overhead.

To address the issues brought about by traditional multi-Engine modes, some teams have proposed a view-level hybrid mode.

3.3.4 View-Level Hybrid Mode#

The core of the view-level hybrid mode is to introduce the concept of windowId for each window, allowing them to share the same Root Isolate. As we mentioned earlier, each isolate has a ui.window singleton object. By making a slight modification to include the ID concept in the Flutter Engine and passing it to the Dart layer, allowing the Dart layer to have multiple windows, we can achieve multiple Flutter Engines sharing one isolate.

As shown in the image below:

View Level Hybrid Mode

This allows for true view-level hybrid development, where multiple FlutterViewControllers can be held simultaneously, and these FlutterVCs can share memory.

However, the drawbacks are also quite evident. We need to modify the Engine code, which leads to high maintenance costs. Additionally, the resource consumption issues of multiple Engines also need to be addressed by continuously trimming the Engines.

4. Engineering Exploration#

4.1 Compilation Modes#

Dart inherently supports two compilation modes: JIT and AOT.

4.1.1 JIT and AOT#

JIT, or Just In Time, is used in Debug mode and allows for dynamic code dispatch and execution, but execution performance is affected by runtime compilation.

JIT

AOT, or Ahead Of Time, is used in Release mode and can generate binary code for specific platforms, offering good execution performance and fast runtime, but requires pre-compilation for each execution, leading to lower development debugging efficiency.

AOT

4.1.2 Debug, Release, Profile#

Correspondingly, Flutter Apps exist in three running modes:

  • Debug
  • Release
  • Profile

Running Modes

Thus, we can see that during the development and debugging process, we need to use the Debug mode that supports JIT, while in production environments, we need to build packages that support AOT in Release mode to ensure performance.

This also imposes certain requirements on our integration and building processes.

4.2 Integration and Building#

Integration refers to incorporating the products of the Flutter Module into the native project in hybrid projects, which can be done in two ways, as follows:

Source Integration vs Product Integration

It can be seen that source integration is required for the Flutter dev branch, while product integration is needed for branches other than Flutter dev. In this case, our hybrid project needs to support both types of integration. Development can be done on the Flutter dev branch with source integration, and then dependencies can be extracted and built into products for remote release, such as building iOS into pods for release to the corresponding Cocoapods repository, and building Android into AAR for release to the corresponding Maven cloud. Other branches can then directly update the Flutter dependency module via gradle or pod install.

Building

Of course, we mentioned that the running modes include Debug, Release, and Profile, and the integrated products will also distinguish these three versions. However, since product integration cannot be debugged, it is meaningless to integrate Debug and Profile versions. Therefore, when extracting and publishing dependencies, it is sufficient to publish only the Release version of the product.

4.3 Workflow#

After setting up the entire "Fan Live" Flutter hybrid project, we have formed an initial Flutter workflow. In the future, we will continue to improve the Flutter hybrid development model and actively participate in the construction of the Flutter ecosystem.

Flutter Workflow

Further Reading:
Exploration of Flutter Hybrid Development Modes
Flutter Boost Hybrid Development Practice and Source Code Analysis

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.