How to Use Flutter for Hybrid Development: Alibaba’s Open Source Code Instance
Apps of a certain scale usually have a set of mature and universal fundamental libraries, especially for apps in the Alibaba system, which generally rely 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.
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. Several ViewControllers can be seen in some specific scenarios, but these scenarios are not discussed here.
We can simply understand this solution in this way: We regard 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 notify the Flutter View to render the current logic page, and then put the Flutter View into the current container.
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, B is currently at the top of the stack. To switch to A, you need to pop B out from the top of the stack. At this time, the status of B is lost. If you want to switch back to B, you can only re-open B and the status of the page cannot be maintained.
And, during the pop process, the official Flutter Dialog may be mistakenly killed. In addition, stack-based operations rely on an attribute modification to the Flutter framework, which makes this solution invasive.
FlutterBoost: A New Generation Hybrid Technology Solution
Flutter Boost project is already made open source in GitHub. Check it out through this link: https://github.com/alibaba/flutter_boost
When Xianyu promotes Flutter, more complex page scenarios have gradually exposed the limitations and problems of the old solution. So we have launched a new hybrid technology solution codenamed FlutterBoost (a nod to the C++ Boost library). The main objectives of the new hybrid solution are as follows:
- Reusable universal hybrid solution
- Support for more complex hybrid modes, such as support for homepage Tab
- Non-invasive solution: The solution of modifying Flutter is no longer relied upon
- Supports 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 uses messages to drive a Flutter page container, thus achieving the synchronization between the Native container and the Flutter container. We hope that the content rendered by Flutter is driven by the Naitve container.
Simply put, we want to make the Flutter container into a browser. We enter a page address, and then the container manages the page rendering. On the Native side, we only need to consider how to initialize the container, and then set the corresponding page flag of the container.
- Container: Native container, platform Controller, Activity, and ViewController
- Container Manager: manager of the container
- Adaptor: Flutter is the adaptation layer
- Messaging: Channel-based message communication
- Container: The container used by Flutter to hold widgets, specifically implemented as the derived class of Navigator.
- Container Manager: To manage Flutter containers and provide APIs, such as Show and Remove.
- Coordinator: The coordinator that receives Messaging messages and is responsible for calling the status management of the Container Manager.
- Messaging: Channel-based message communication
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 weaken and abstract away 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 as the container content. Therefore, the Native container should prevail when we understand and perform routing operations. The Flutter Widget depends on the status of the Native page container.
Then, when talking about pages in the FlutterBoost, we refer to the Native container and its affiliated widgets. All page routing operations, as well as opening or closing pages, are actually direct operations on Native page containers. No matter where the routing request comes from, it will eventually be forwarded to Native to implement the routing operation. This is also the reason why the Platform protocol needs to be implemented when FlutterBoost is accessed.
On the other hand, we cannot control the service code to push new widgets through the Navigator of the Flutter itself. If the service uses Navigator directly to operate widgets without using FlutterBoost, including non-full screen widgets, such as Dialog, we recommend that the service itself manages its status. This type of widget does not belong to the page defined by FlutterBoost.
Understanding the page concept here is critical to understanding and using FlutterBoost.
Main Differences from the Old Solution
We mentioned earlier that the old solution maintains a single Navigator stack structure at the Dart layer for widget switching. The new solution introduces the Container concept on the Dart side. Instead of using 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 the search and switching of pages, and is no longer subject to the top stack operation. Therefore, some previous 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?
Implementation of Multiple Navigators
Flutter provides an interface for customizing 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 page contained in this Navigator is the page corresponding to the currently visible container.
Native containers and Flutter containers (Navigators) correspond to each other one by one, and their lifecycles are also synchronized. When a Native container is created, a Flutter container is also created, and they are linked by the same ID. When the Native container is destroyed, the Flutter container is also destroyed. The status of the Flutter container is dependent on the Native container, which is what we call Native-driven. The Manager centrally manages and switches the containers currently 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 notifies the Flutter Coordinator through the message mechanism that a new container is created.
- The Flutter Container Manager is then notified to create the corresponding Flutter container, and load the corresponding widget page in it.
- When the Native container is displayed on the screen, the container sends a message to Flutter Coordinator notifying the ID of the page to be displayed.
- The Flutter Container Manager finds the Flutter Container of the corresponding ID, and sets it as a visible container on the foreground.
This is the main logic for creating a new page. Operations, such as destroying and entering the background, are also driven by Native container events.
Officially Proposed Hybrid Solution
How It Works
The Flutter technology chain is mainly composed of the Flutter Engine implemented by C++ and the Framework implemented by Dart (the compilation and construction tools is not discussed here). The Flutter Engine is responsible for thread management, Dart VM status management, and Dart code loading. The Framework implemented by Dart code is the main API that services are exposed to. Concepts, such as widgets, are the content of the Framework at the Dart level.
At most, only one Dart VM can be initialized in a process. However, a process can have multiple Flutter Engines, and multiple Engine instances share the same Dart VM.
Let’s take a look at the specific implementation. Every time a FlutterViewController is initialized on iOS, an engine will be initialized, which means a new thread (theoretically, the thread can be reused) will run the Dart code. Similar effects can be achieved for Activity like Android. If multiple Engine instances are started, note that the Dart VM is still shared, but the code loaded by different Engine instances runs in their independent Isolate.
Deep Engine Sharing
In terms of hybrid solutions, we have discussed with Google and come up with some possible solutions. The official recommendation 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 master Isolate.
However, the official long-term recommendation are not currently well supported.
The main problem to solve in the hybrid solution is how to deal with the alternate Flutter and Native pages. Google engineers provide a Keep It Simple solution: For continuous Flutter pages (widgets), only the current FlutterViewController needs to be opened. For the alternate Flutter pages, a new engine is initialized.
For example, let’s perform the following navigation operations:
Flutter Page1 -> Flutter Page2 -> Native Page1 -> Flutter Page3
We only need to create different Flutter instances in Flutter Page1 and Flutter Page3.
The advantage of this solution is that it is easy to understand and logically sound, but potential problems also exist. If a Native page and a Flutter page are alternated all the time, the number of Flutter Engines increases linearly, and the Flutter Engine itself is a heavy object.
Problems of the Multi-Engine Mode
- Redundant resources: In the multi-engine mode, the Isolates between each engine are independent of each other. Logically, this does not cause any harm, but the underlying layer of the engine actually maintains the image cache and other memory-consuming objects. Imagine that each engine maintains its own image cache, which can be very memory intensive.
- Plug-in registration: Plug-ins rely on Messenger to transmit messages, while Messenger is currently 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 target of message transmission will become uncontrollable.
- Page differences between Flutter Widget and Native: Flutter pages are widgets, and Native pages are VC. Logically, we want to eliminate the differences between the Flutter page and the Native page. Otherwise, unnecessary complexity appears when we perform page tracking and other unified operations.
- Increased complexity of communication between pages: If all Dart code runs in the same engine instance and they share an Isolate, a unified programming framework can be used for inter-widget communication. And, multi-engine instances also make this case more complex.
Therefore, we have not adopted the multi-engine hybrid solution in comprehensive consideration.
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.
From the very beginning of the project, we hoped that FlutterBoost could solve the general problem of Native App hybrid mode access to Flutter. So we have made it a reusable Flutter plug-in, hoping to attract more interested people to participate in the construction of the Flutter community. In this limited space, we shared the experience and code accumulated by Xianyu in the Flutter hybrid technology solution. People who are interested are welcome to actively communicate with us.
Extension and Supplement
When switching between two Flutter pages, we only have one Flutter View, so we need to save screenshots of the previous page. If the Flutter page contains multiple screenshots, it will occupy a large amount of memory. Here, we adopt the file memory L2 cache policy, in which only 2–3 screenshots are saved at most, and the rest of the written files are loaded on demand. In this way, we can maintain a stable level in the memory while ensuring the user experience.
In terms of page rendering performance, the advantages of Flutter AOT are obvious. During fast page switching, Flutter can switch corresponding pages sensitively, logically creating a sense of Flutter with multiple pages.
Support for Release1.0
At the beginning of the project, we developed based on the Flutter version currently used by Xianyu, and then 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 to complete the access. For detailed access documentation, see the official project documentation on the GitHub homepage.
The Flutter Boost project is already made open source in GitHub. Check it out through this link: https://github.com/alibaba/flutter_boost
(Original article by Chen Jidong)