Flutter Analysis and Practice: Using Hybrid Stack Framework for Development
1.5.1 Why Is a Hybrid Solution Needed?
Apps of a certain scale usually have a set of mature and universal fundamental libraries, especially for apps in the Alibaba system, which generally depend on many fundamental libraries in the system. The cost and risk of using Flutter to re-develop an app from scratch are high. Therefore, incremental migration in native apps is a robust way for Flutter technology to be applied in existing native apps. The tech team from Xianyu, Alibaba’s second-hand trading platform, has developed a unique hybrid technology solution in this practice. During the process, Xianyu has closely communicated with the Google Flutter team, and selected the solutions and implementation methods based on the Google Flutter team’s suggestions and actual business situation.
1.5.2 Google’s Hybrid Solution
18.104.22.168 How It Works
The Flutter technology chain is mainly composed of the Flutter engine written in C++ and the Flutter framework written in Dart. The Flutter engine is responsible for thread management, Dart VM status management, and Dart code loading. The Flutter framework is the main API that exposes services externally. Concepts, such as widgets, are the Flutter framework content at the Dart level.
Only one Dart VM can be initialized in a process. However, a process can involve multiple Flutter engines, and multiple Flutter engines share the same Dart VM.
Let’s look at the specific implementation. Every time a
FlutterViewController is initialized for an iOS project, an engine will be initialized, which means that a new thread (theoretically, the thread can be reused) will run the Dart code. Similar effects can be achieved for activities on Android. If multiple Flutter engines are started, the Dart VM is still shared, but the code loaded by different Flutter engines runs in their separate isolates.
22.214.171.124 Recommendations from Google
1) Deep Engine Sharing
The recommendation from Google for Flutter is that, in the long run, the ability to support multi-window rendering in the same engine should be supported. At least logically, FlutterViewController shares the resources of the same engine. In other words, we want all the rendering windows to share the same primary isolate.
However, Google’s official long-term recommendation is not well supported.
2) Multi-Engine Mode
In the hybrid solution, the main problem to be solved is how to deal with the alternate Flutter and native pages. Google engineers provide a Keep It Simple solution: Display continuous Flutter pages (widgets) only in the current
FlutterViewController. For alternate Flutter pages, initialize a new engine.
For example, let’s perform the following navigation operations:
Flutter Page1 -> Flutter Page2 -> Native Page1 -> Flutter Page3
We only need to initialize a Flutter engine for Flutter Page1 and initialize another one for Flutter Page3.
This solution is easy to understand. However, if a native page and a Flutter page alternate all the time, the number of Flutter engines increases linearly, but the Flutter engine itself is a complex object.
3) Problems of Multi-Engine Mode
- Redundant Resources: In multi-engine mode, the isolates between Flutter engines are independent of each other. Logically, this does not cause any harm, but the Flutter engine needs to maintain objects such as image caches, which is memory consuming. If each Flutter engine maintains its own image caches, it must be very memory intensive.
- Plug-In Registration: Plug-ins rely on Messenger to transmit messages, while Messenger is implemented by FlutterViewController (Activity). If you have multiple FlutterViewControllers, the registration and communication of plug-ins will become chaotic and difficult to maintain, and the source and destination of message transmission will be uncontrollable.
- Differences between Flutter Widgets and Native Pages: Flutter pages are widgets, and native pages are ViewControllers. Logically, we want to eliminate the differences between Flutter and native pages. Otherwise, we may encounter extra complexity when performing page tracking and other unified operations.
- Increased Complexity of Communication between Pages: Multiple Flutter engines also make the communication more complex. However, if all Dart code runs on the same Flutter engine and shares an isolate, a unified programming framework can be used for inter-widget communication.
Therefore, Xianyu has not adopted the multi-engine hybrid solution in comprehensive consideration.
126.96.36.199 Status Quo and Thoughts
The hybrid solution currently used by Xianyu is to share the same engine. This solution is based on the fact that only one page at most can be seen at any time. However, in specific scenarios, several ViewControllers can be seen, which will not be discussed here.
We can simply understand this solution in this way: Consider the shared Flutter View as a canvas, and then use a native container as the logic page. Every time we open a container, we use the communication mechanism to instruct the Flutter View to render the current logic page, and then put the Flutter View into the current container.
The old solution maintains a Navigator stack at the Dart layer, as shown in Figure 1–20. This solution cannot support multiple horizontal logic pages at the same time, because you must perform operations from the top of the stack during page switching, and cannot perform horizontal switching while maintaining the status.
For example, for two pages A and B, page B is currently at the top of the stack. To switch to page A, you need to pop page B out from the top of the stack. At this time, the status of page B is lost. If you want to switch back to page B, you can only re-open page B. The earlier status of the page cannot be maintained. This is the biggest limitation of the old solution.
During the pop process, the official Flutter Dialog may be mistakenly killed.
In addition, stack-based operations rely on attribute modifications to the Flutter framework, which makes this solution invasive.
1.5.3 FlutterBoost: New-Generation Hybrid Technology Solution
188.8.131.52 Refactoring Plan
When Xianyu promotes Flutter, more complex page scenarios have gradually exposed the limitations and problems of the old solution. Therefore, Xianyu has launched a new hybrid technology solution codenamed FlutterBoost. The main objectives of the new hybrid solution are:
- Reusable universal hybrid solution
- Support for more complex hybrid modes, such as support for the homepage tab
- Non-invasive solution rather than relying on the solution of modifying Flutter
- Support for universal page lifecycle
- Unified and clear design concepts
Similar to the old solution, the new solution still adopts the shared engine mode. The main idea is that a native container drives a Flutter page container by using messages, which achieves the synchronization between the native container and the Flutter container. We want the content rendered by Flutter to be driven by the native container.
Simply put, we want to make the Flutter container into a browser. After a page address is entered, the container manages page rendering. On the native side, developers only need to consider how to initialize the container, and then set the corresponding page tag of the container.
1) Native Layer
- Container: contains the native container, platform Controller, Activity, and
- Container Manager: is the manager of the container.
- Adaptor: uses Flutter for adaptation.
- Messaging: implements channel-based messaging.
2) Dart Layer
- Container: is used by Flutter to hold widgets, which is implemented as the derived class of Navigator.
- Container Manager: manages Flutter containers and provides APIs such as Show and Remove.
- Coordinator: receives messages from Messaging and calls status management of Container Manager.
- Messaging: implements channel-based messaging.
3) Understanding of Pages
The object and concept of the page expressed in native and Flutter are inconsistent. In native, a page is generally expressed as a
ViewController and an Activity. However, in Flutter, a page is expressed as a widget. We want to unify the concept of pages or abstract the concept of pages corresponding to the widgets in Flutter. In other words, when a native page container exists,
FlutterBoost ensures that a widget is used in the container. Therefore, the native container should prevail for when we understand and perform routing operations. Flutter widgets depend on the status of the native page container.
Then, when we are talking about pages in the
FlutterBoost, we refer to the native container and its affiliated widgets. All page routing operations, including opening or closing pages, are performed in native page containers. No matter where the routing request comes from, it will eventually be forwarded to native page containers to implement the routing operation. This is why the platform protocol needs to be implemented when
FlutterBoost is accessed.
In addition, we cannot control the business code to push new widgets through Navigator of Flutter. If a business uses Navigator to operate widgets without using
FlutterBoost, we recommend that the business manage its status. This type of widget does not belong to pages defined by
The page concepts are critical for understanding and using
184.108.40.206 Main Differences from the Old Solution
The old solution maintains a single Navigator stack structure at the Dart layer for widget switching. The new solution introduces the container concept at the Dart layer. Instead of using the stack structure to maintain existing pages, all current pages are maintained in the form of flat key-value mapping, and each page has a unique ID. This structure naturally supports page searching and switching and is no longer subject to operations on the top of the stack. Therefore, some problems caused by pop can be solved. Moreover, the page stack operation does not need to be performed by modifying the Flutter source code, eliminating the intrusiveness of the implementation.
In fact, the container we introduced is the Navigator, that is, a native container corresponds to a Navigator. How does this work?
Flutter provides an interface for developers to customize Navigators at the underlying layer and we have implemented an object for managing multiple Navigators. Currently, only one visible Flutter Navigator is available at most. The pages contained in this Navigator are the pages corresponding to the visible container.
Native containers map Flutter containers (Navigators) one by one, and their lifecycles are also synchronized. When a native container is created, a Flutter container is also created, and they are associated by the same ID. When the native container is destroyed, the Flutter container is also destroyed. The status of a Flutter container varies with a native container, which is native-driven. Manager centrally manages and switches the containers displayed on the screen.
Let’s use a simple example to describe the process of creating a new page:
- Create a native container (iOS ViewController, Android Activity, or Fragment.)
- The native container uses the messaging mechanism to notify the Flutter Coordinator that a container is created.
- Flutter Container Manager receives the notification, creates the corresponding Flutter container, and loads the corresponding widget page in it.
- When the native container is displayed on the screen, the container notifies the Flutter Coordinator of the ID of the page to be displayed.
- Flutter Container Manager finds the Flutter container of the corresponding ID and sets it as a visible frontend container.
This is the main logic for creating a page. Operations such as destroying and entering the backend are also driven by native container events.
Currently, FlutterBoost has been supporting all Flutter-based development services on the Xianyu client in the production environment, providing support for more complex hybrid scenarios, and stably providing services for hundreds of millions of users.
At the very beginning of the project, Xianyu wanted FlutterBoost to solve the general problem of connecting a native app to Flutter in hybrid mode. Therefore, FlutterBoost is made as a reusable Flutter plug-in, expecting to attract more users to participate in the construction of the Flutter community. Xianyu’s solution may not be the best. However, Xianyu hopes that you provide more excellent components and solutions in the community.
1.5.4 Extensions and Supplements
When switching between two Flutter pages, we have only one Flutter View, so we need to save a screenshot of the previous page. If there are many Flutter pages, the screenshots occupy a large amount of memory. Therefore, the L2 cache policy of file memory is used, in which only two or three screenshots can be saved. Other screenshots will be loaded as needed when written to files. In this way, we can maintain stable memory usage while ensuring the user experience.
In terms of page rendering performance, the advantages of Flutter ahead-of-time (AOT) are obvious. During fast page switching, Flutter can switch pages flexibly, logically creating a sense of Flutter with multiple pages.
220.127.116.11 Support for Release 1.0
At the beginning of the project, we developed based on the Flutter version currently used by Xianyu, and conducted the Release 1.0 compatibility upgrade test. No problems have been found so far.
Any project integrated with Flutter can easily introduce FlutterBoost as a plug-in in an officially dependent way and only a small amount of code access is required for the project. For more information about access documentation, see the official project documentation on the GitHub homepage.