In practical working scenarios, it is difficult for us to build a project with pure Flutter from scratch. Just because of this, the jump management of Native+Flutter hybrid stack has to be the first consideration in mixed development, because it is difficult to ensure that we will not encounter the following situations.

After all, the ecology of Flutter is not very mature. There are not many ready-made solutions and wheels, and they may not be easy to use. They either occupy too much resources or are too intrusive. Fortunately, after these days of groping, summed up a set of plans for everyone to learn reference, mutual exchange.


In accordance with international practice, I will first introduce some solutions and existing problems on the market.

This article is based on Flutter version: 2.2 & Platform: Android

1.Google Official (Multi-engine Solution)

The Widget tree is rendered one new FlutterEngine at a time. Although the overhead of creating Flutterengines after Flutter 2.0 has been greatly reduced, it is still not solved that each FlutterEngine is a separate isolate. If data needs to be exchanged between Flutter① and Flutter②, it will be very troublesome. We also cannot guarantee that there will be no data interaction between them, so Pass.

2. Flutter_boost (single engine)

Flutter_boost recently released the bate version 3.0, eliminating the engine intrusion of version 2.0 (thumbs up!). But there are still many problems:

① : The contradiction between the high number of not closed issues and the reply is not timely (understandable, there is still work to be done).

② : Complex design, when the use of problems, by changing the source code flutter_boost to solve the cost is very high.

③ The aspect of the flutter is highly coupled. I had to use a series of route-related tools and widgets provided by Flutter_Boost to mix stack jumps. If I wanted to change the framework later, the code changes would be massive.

And succeeded in persuading me to quit.

3. Hello Bike team flutter_thrio (Single engine scheme)

The advantages and disadvantages of the library has been very detailed, here will not repeat, interested friends can enter the portal to see.

4. The Isolate reuse scheme of Bytedance team and the TRouter scheme of Tencent Xinyue team

Unfortunately, neither of these solutions is currently open source, but it’s likely that the Byte team’s solution is quite intrusive.


Since there is no existing solution, let’s roll up our sleeves and build one. Here is a demonstration of the effect of a hybrid stack jump:

Project address: github.com/wangkunhui/…

So start implementing the above functions.

First, determine what you want to achieve and work towards it:

Goal 1: Reuse FlutterEngine to avoid additional resource overhead and communication costs between FlutterEngine.

Goal 2: Jumps between Flutter widgets do not need to be controlled via the Native layer (required by Flutter_boost).

Goal 3: Each open Flutter can and can only manage its own stack. After all the widgets of the current Flutter are removed from the stack, the current Flutter exits.

Open Native pages with Flutter parameters (return values are also supported, but not yet added).

Based on these goals, the model diagram is as follows:

The diagram above shows the stack information of five activities that are opened in turn. It can be divided into three columns, which are explained as follows:

On the left is the stack information for the Activity, where the second and fourth stacks mount the Flutter and open several Flutter widgets, respectively.

In the middle is A diagram of the Widget stack in FlutterEngine. Due to the reuse of FlutterEngine, Widgets A and P all exist in the same stack, while at the bottom is A blank HostWidget that is not responsible for any business logic. The purpose of the Flutter Navigator is to ensure that the business-related widgets above the Flutter Navigator can be popped out of the stack properly (because the last Widget of the Flutter Navigator cannot be popped).

On the right is the Widget stack in the middle of the FlutterEngine, which widgets are associated with the HostActivity instances so that I can control when I unstack the widgets. Finish to drop the associated Activity when you know which Widget to retreat to.

If the above description is not intuitive, I will explain that I opened two Flutter pages in HostActivity instance 2, Widget O and Widget P. When the two widgets complete their tasks, they will exit. When I exit Widget P, The interface becomes Widget O, which is fine, then I continue to exit Widget O, and if nothing special is done, FlutterEngine will display Widget N, which is obviously not what we want. We want to finish the HostActivity instance 2 and show NativeOneActivity. To do this, You need to make a special distinction between the widgets in the FlutterEngine stack that are displayed in different HostActivity instances.

Ok, the theoretical work has been finished, and the next is the happy Coding time. We are faced with the following task list:

How to reuse FlutterEngine?

2. FlutterActivity, FlutterFragment or FlutterView?

How to monitor FlutterEngine stack changes?

How to synchronize listening stack information to the host Activity instance?

How to synchronize the host Activity with the Flutter page?


First, instantiate FlutterEngine first:

/** * Initializes FlutterEngine *@paramContext *@paramBlock initialization callback 0 no initialization required 1 Initialization started 2 Initialization completed */
@Synchronized
fun initFlutterEngine(context: Context, block: (status: Int) - >Unit) {
    // Check that the cache exists
    if(! FlutterEngineCache.getInstance().contains(FLUTTER_ENGINE)) { block(1)
        // Initialize FlutterEngine
        var engine = FlutterEngine(context.applicationContext)
        // Initialize BasicMessageChannel
        messageChannel = BasicMessageChannel(
             engine.dartExecutor,
             FLUTTER_MIX_STACK_CHANNEL,
             StringCodec.INSTANCE
        )
        // Add a message listenermessageChannel? .setMessageHandler(messageHandler)this.initCallback = block
        // Start running the DART code
        engine.dartExecutor.executeDartEntrypoint(
               DartExecutor.DartEntrypoint.createDefault()
        )					
        / / the cache FlutterEngine
        FlutterEngineCache.getInstance().put(FLUTTER_ENGINE, engine)
    } else {
        block(0)}}Copy the code

FlutterEngineCache encapsulates the cache functionality of FlutterEngine, but how do you use it?

FlutterActivity provides a static method withCachedEngine to retrieve an Intent instance that uses a cache. You can also override the getCachedEngineId method with FlutterActivity. To achieve the effect of FlutterEngine reuse, FlutterActivity can be directly launched to load Flutter pages. It is very simple and convenient, but if there are some complicated scenes, FlutterActivity is not enough.

Second, Flutter 2.0 provides us with three types of Flutter containers, namely FlutterActivity (FlutterFragmentActivity), FlutterFragment and FlutterView. In order to meet our requirements in different scenarios, it is highly recommended that we directly use FlutterActivity from the richness of the annotation documents of the three classes. A brief look at the source code shows that both FlutterActivity and FLutterFragment are implemented based on FlutterView. FlutterView is more flexible if the project has a specified base class that needs to be inherited, or if you want to implement native UI+Flutter UI, or if you want to warm up the Flutter Widget earlier, so I chose normal Activity+FlutterView. The pseudocode is as follows:

class HostActivity : AppCompatActivity() {var flutterEngine // Get FlutterEngine in cache
  var flutterChannel // BasicMessageChannel is explained below
  var flutterView 
  onCreate(){
    // Preheat the FlutterEngine at the beginning of the onCreate method call to make Widget loading smoother
    flutterEngine.lifecycleChannel.appIsResumed()
    // Also send a message to FluuterEngine in onCreate to replace the Widget we want to load in advance
    flutterChannel.sendMessage(routePath)
    super.onCreate()
    setContentView(flutterView = createFlutterView())
  }
  
  onResume(){
    super()
    flutterEngine.lifecycleChannel.appIsResumed()
  }
  
  onPause(){
    super()
    flutterEngine.lifecycleChannel.appIsPaused()
  }
  
  onDestroy(){
    super()
    flutterView.detachFromFlutterEngine()
  }
}
Copy the code

Observers for Widget stack changes in FlutterEngine. When MaterialApp is initialized, there is a parameter, navigatorObservers, Allows us to add changes to navigator, the page-switching class that Flutter provides.

void main() {
  // The Get framework is used to demonstrate this, but it is not required
  var getApp = GetMaterialApp(
    initialRoute: RouterMapper.ROUTER_HOME,
    getPages: [
      GetPage(
          name: RouterMapper.ROUTER_HOST, // Empty HostViewpage: () => Host(), transition: Transition.rightToLeft), ... ] , navigatorObservers: [RouteNavigatorObserver()], ); runApp(getApp); RouterHelper.registerApp();// Register message listening here
}

class RouteNavigatorObserver extends NavigatorObserver {
  @override
  void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
  	// There are widgets that can be pushed to fetch name information from route and synchronize it to Native
  }

  @override
  void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) {
    // There are widgets out of the stack that can fetch name information from route and synchronize it to Native
  }

  @override
  void didReplace({Route<dynamic>? newRoute, Route<dynamic>? oldRoute}) {
    // The new Widget replaces the previous Widget that gets name information from Newroute and oldroute and synchronizes it to Native
  }

  @override
  void didRemove(Route<dynamic> route, Route<dynamic>? previousRoute) {
    // A Widget has been removed. This is generally not recommended}}Copy the code

So, how do we synchronize messages to Native, once we implement monitoring for route changes in FlutterEngine by inheriting NavigatorObserver and registering them with navigatorObservers?

** Fourth question, **Flutter provides three ways to interact with Native data, namely, BasicMessageChannel, MethodChannel and EventChannel. The first two of them can transfer data bidirectionally. EventChannel only supports Native to transmit data to a Flutter, as a notification of system messages. MethodChannel is used to pass method calls, and BasicMessageChannel is used to pass binary data, which is more suitable for our trial scenario. Next we need the BasicMessageChannel residing in the Flutter and Native layers for data interaction.

Create a BasicMessageChannel on the Flutter and Native layers:

//Flutter
class RouterHelper {
  // Register the message channel
  static final _routerMessageChannel =
      BasicMessageChannel<String?> ("flutter_router_channel", StringCodec());

  static registerApp() {
    // Prevent empty exceptions below
    WidgetsFlutterBinding.ensureInitialized();
    // Register a message listener
    _routerMessageChannel.setMessageHandler((String? message) async {
      if(message ! =null) {
        // The message from the native layer}}); }}Copy the code
//Kotlin
fun initMessageChannel(a){
  var messageChannel = BasicMessageChannel(
      engine.dartExecutor,
      "flutter_router_channel",
      StringCodec.INSTANCE)
  
  messageChannel.setMessageHandler{ message, reply ->
      // Process messages sent by Flutter}}Copy the code

Message sending is realized by the Send method provided by BasicMessageChannel. In the above question, we have detected the change information of the page in the Flutter by listening on the Navigator. Next, we need to send the corresponding information to the Native layer for processing in the listening method. In the Native layer, we can abstract out an interface, let HostActivity implement this interface, and distribute messages to the current active HostActivity through interface-oriented programming. HostActivity maintains a stack that records stack information changes within a Flutter.

/** * Callback to flutter route changes */
interface RouteCallback {
		//flutter route pushes messages
    fun onPush(route: String)
		// the flutter route exits the stack message
    fun onPop(route: String)
		// the flutter route stack replaces messages
    fun onReplace(newRoute: String, oldRoute: String)
		// Message that a flutter route was removed
    fun onRemove(route: String)
		// Flutter applies to open Native page messages
    fun routeNative(nativeRoute: String, params: HashMap<String, Any>? = null)
		// the flutter route exits the stack message
    fun getLifecycleOwner(a): LifecycleOwner
}

class HostActivity : AppCompatActivity(), RouteCallback {
	  // Record the routing stack information of a FLUTTER
    private val routeStack: Stack<String> by lazy {
        Stack()
    }
		
    TODO RouteCallback {TODO RouteCallback {TODO RouteCallback {TODO RouteCallback
}
Copy the code

Finally, we need to synchronize the Widget page with the HostActivity page when the user returns to the HostActivity. For example, when the HostActivity only opens one Widget, the HostActivity also needs to synchronize finish when the Widget exits. Even if there are other widgets in FlutterEngine. We can do this through the routeStack maintained in HostActivity.

override fun onBackPressed(a) {
 		// Determine the current stack length. If the current HostActivity stack length is less than or equal to 1, then the Activity finsih
    if (routeStack.size <= 1) {
      super.onBackPressed()
      flutterEngine.navigationChannel.popRoute()
    } 
    // Perform the Navigator pop operation normally
    else {
      flutterEngine.navigationChannel.popRoute()
    }
}

Copy the code

There is also a problem to note that the return of some widgets is not handled by the return key, so we need to do some special processing for the onPop method:

override fun onPop(route: String) {
    // Handle the stack logic normally
    if(routeStack.isEmpty() && ! isFinishing) {// If all the HostActivity has been removed from the stack, the current HostActivity service is complete
      	finish()
    }
}
Copy the code

In this way, you can associate hostactivities with widgets that are open in the host. The main logic is implemented without responsibility, and there is no need to use any third party framework. This gives you great flexibility, especially for Flutter, whose ecology is not yet mature. Avoid overly intrusive frameworks and leave plenty of room for future iterations of the technology.


Shortcomings and deficiencies

Although mixed stack management can be done by monitoring the changes of stack information inside Flutter and message synchronization through BasicMessageChannel, there are still some problems that need to be solved.

OneActivity starts a HostActivity and wants to give OneActivity a return value when the HostActivity exits. This function has not been implemented in the sample code. We will continue to improve the framework when we are free.

2: The “cross-domain” problem is a name I defined myself. If I have two hostActivities open, the second HostActivity wants to close the Widget opened in the first HostActivity through the Navigator’s remove method. This can be done because the two hostActivities use the same FlutterEngine. This will cause the first HostActivity’s page to display incorrectly. In fact, I am thinking that the ideal hybrid management framework would be to treat each opened HostActivity as a WebView. It is obviously not appropriate to close the page opened in the first WebView in the second WebView. It is also worth considering whether such cross-domain returns need to be supported.


Well, that’s all for this article. The solution is still in the demo stage and is not yet fully developed into a high availability framework. It just provides an idea for managing the hybrid stack in hybrid development