Introduce Flutter Boost

As we all know, Flutter is a cross-platform technology Framework consisting of the Flutter Engine implemented by C++ and the Framework implemented by Dart. The Flutter Engine is responsible for thread management, Dart VM state management, and Dart code loading, while the Framework implemented by the Dart code is responsible for upper-level business development. Concepts such as the components provided by Flutter are included in the Framework.

With the development of Flutter, more and more apps in China begin to access Flutter. To reduce the risk, most apps adopt a progressive approach to incorporating Flutter. Select several pages in your App and write them with Flutter. However, they all encounter the same problem: how to manage routing when native pages and Flutter pages coexist. Switching and communication between native pages and Flutter pages are all problems to be solved in hybrid development. However, there is no clear solution, only that in mixed development, developers should use the same engine’s ability to support multi-window drawing, at least logically so that FlutterViewController shares resources in the same engine. In other words, the authorities wanted all drawing Windows to share the same main Isolate, rather than having multiple main isolates. However, Flutter officials have not provided a good solution to the current multi-engine mode problems. In addition to the high memory consumption, the multi-engine model also presents the following problems.

  • Redundant resources. In multi-engine mode, each engine’s Isolate is independent of each other. Although this is not harmful logically, each engine maintains a set of memory consuming objects such as image caches, so the device’s memory consumption is very severe.
  • Plug-in registration problem. In the Flutter plugin, message passing relies on Messenger, which is implemented by the FlutterViewController. If multiple FlutterViewControllers exist in an application at the same time, plug-in registration and communication can become confusing and difficult to maintain.
  • Differences between Flutter components and native pages. Usually, Flutter pages are made up of components, while native pages are made up of viewControllers or activities. Logically, we want to eliminate the difference between the Flutter page and the native page in order to create some extra work for page burying and other operations.
  • Increase the complexity of page communication. If all the Dart code runs in the same engine instance, they share the same Isolate and can communicate with each other using a unified framework, but having multiple engine instances makes managing the Isolate more complicated.

If the multi-engine problem is not addressed, the navigation stack for the mixed project looks like the figure below.At present, there are two solutions to the problem of multi-engine mode of native Flutter engineering, one is bytedance’s modification of the source code of Flutter Engine, the other is Xianyu’s open source FlutterBoost. Since ByteDance’s hybrid solution is not open source, the only solution available is FlutterBoost.

FlutterBoost is a reusable page plugin developed by The Free Fish Technology team that aims to make Flutter containers a browser – like loading solution. To this end, the Xianyu technical team hopes that FlutterBoost can complete the following basic functions:

  • Reusable general-purpose hybrid development solutions.
  • Support for more complex blending modes, such as tab-switching scenarios.
  • A non-invasive solution that does not rely on modification of the Flutter solution.
  • Supports unified management of the page life cycle.
  • Unified and clear design concept.

Also, a recent Update to Flutter Boost, version 3.0, brings the following updates:

  • It does not invade the engine and is compatible with all versions of Flutter. Upgrading the Flutter SDK does not require upgrading FlutterBoost, greatly reducing the upgrade cost.
  • No distinction between Androidx and Support branches.
  • Simplified architecture and interfaces, with half the code compared to FlutterBoost2.0.
  • Dual-end unification, including interface and design unification.
  • Support to open the Flutter page without opening the container scene.
  • Notification of page lifecycle changes is more convenient for business use.
  • Solve the legacy problems in 2.0, such as difficult Fragment access, unable to transfer data after the page is closed, Dispose does not execute, too high memory usage.

Ii. Flutter Boost integration

To integrate Flutter Boost into a native project, simply treat Flutter Boost as a plug-in project. As with other Flutter plug-ins, dependencies need to be added before using FlutterBoost. Open the Flutter project of the hybrid Project using Android Studio and add the FlutterBoost dependency plugin to pubspec.yaml, as shown below.

flutter_boost:
    git:
        url: 'https://github.com/alibaba/flutter_boost.git'
        ref: 'v3.0 - hotfixes'
Copy the code

It should be noted that the version of FlutterBoost relied on here corresponds to the version of Flutter. Otherwise, the version mismatch will occur. The FlutterBoost plug-in is then pulled locally using the flutter Packages get command.

2.1 the Android integration

Open the new native Android project using Android Studio and add the following code to the settings.gradle file of the native Android project.

setBinding(new Binding([gradle: this]))
evaluate(new File(
  settingsDir.parentFile,
  'flutter_library/.android/include_flutter.groovy'))
Copy the code

Then, open the build.gradle file in the native Android project app directory and add the following dependency scripts.

dependencies {
  implementation project(':flutter_boost')
  implementation project(':flutter')
}
Copy the code

Recompile to build the native Android project, and if there are no errors Android has successfully integrated FlutterBoost. Before using Flutter Boost, initialization needs to be performed. Open the native Android project, create a new Application that inherits FlutterApplication, and initialize FlutterBoost in the onCreate() method.

public class MyApplication extends FlutterApplication {


    @Override
    public void onCreate(a) {
        super.onCreate(a); FlutterBoost.instance().setup(this.new FlutterBoostDelegate() {

            @Override
            public void pushNativeRoute(String pageName, HashMap<String, String> arguments) {
                Intent intent = new Intent(FlutterBoost.instance().currentActivity(), NativePageActivity.class);
                FlutterBoost.instance().currentActivity().startActivity(intent);
            }

            @Override
            public void pushFlutterRoute(String pageName, HashMap<String, String> arguments) {
                Intent intent = new FlutterBoostActivity.CachedEngineIntentBuilder(FlutterBoostActivity.class, FlutterBoost.ENGINE_ID)
                        .backgroundMode(FlutterActivityLaunchConfigs.BackgroundMode.opaque)
                        .destroyEngineWithActivity(false).url(pageName)
                        .urlParams(arguments)
                        .build(FlutterBoost.instance().currentActivity());
                FlutterBoost.instance().currentActivity().startActivity(intent);
            }

        },engine->{
            engine.getPlugins(a); }); }}Copy the code

Then, open the androidmanifest.xml file under the native Android project and replace Application with the custom MyApplication, as shown below.

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          xmlns:tools="http://schemas.android.com/tools"
          package="com.idlefish.flutterboost.example">

    <application
        android:name="com.idlefish.flutterboost.example.MyApplication"
        android:label="flutter_boost_example"
        android:icon="@mipmap/ic_launcher">

        <activity
            android:name="com.idlefish.flutterboost.containers.FlutterBoostActivity"
            android:theme="@style/Theme.AppCompat"
            android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density"
            android:hardwareAccelerated="true"
            android:windowSoftInputMode="adjustResize" >
            <meta-data android:name="io.flutter.embedding.android.SplashScreenDrawable" android:resource="@drawable/launch_background"/>

        </activity>
        <meta-data android:name="flutterEmbedding"
                   android:value="2">
        </meta-data>
    </application>
</manifest>
Copy the code

Since Flutter Boost is integrated into the Native Android project as a plugin, we can open and close the Page of the Flutter module in Native.

FlutterBoost.instance().open("flutterPage",params);
FlutterBoost.instance().close("uniqueId");
Copy the code

The use of the Flutter Dart is as follows. First, we can initialize it in the main() method of the program entry to the main.dart file.

void main(a) {
  runApp(MyApp());
}
class MyApp extends StatefulWidget {
  @override
  _MyAppState createState(a) => _MyAppState();
}
class _MyAppState extends State<MyApp> {
   static Map<String, FlutterBoostRouteFactory>
	   routerMap = {
    '/': (settings, uniqueId) {
      return PageRouteBuilder<dynamic>(
          settings: settings, pageBuilder: (_, __, ___)
          => Container());
    },
    'embedded': (settings, uniqueId) {
      return PageRouteBuilder<dynamic>(
          settings: settings,
          pageBuilder: (_, __, ___) =>
          EmbeddedFirstRouteWidget());
    },
    'presentFlutterPage': (settings, uniqueId) {
      return PageRouteBuilder<dynamic>(
          settings: settings,
          pageBuilder: (_, __, ___) =>
          FlutterRouteWidget(
                params: settings.arguments,
                uniqueId: uniqueId,
              ));
    }};
   Route<dynamic> routeFactory(RouteSettings settings, String uniqueId) {
    FlutterBoostRouteFactory func =routerMap[settings.name];
    if (func == null) {
      return null;
    }
    return func(settings, uniqueId);
  }

  @override
  void initState(a) {
    super.initState(a); } @override
  Widget build(BuildContext context) {
    return FlutterBoostApp(
      routeFactory
    );
  }
Copy the code

Of course, you can also listen for the life cycle of the page, as shown below.

class SimpleWidget extends StatefulWidget {
  final Map params;
  final String messages;
  final String uniqueId;

  const SimpleWidget(this.uniqueId, this.params, this.messages);

  @override
  _SimpleWidgetState createState(a) => _SimpleWidgetState();
}

class _SimpleWidgetState extends State<SimpleWidget>
    with PageVisibilityObserver {
  static const String _kTag = 'xlog';
  @override
  void didChangeDependencies(a) {
    super.didChangeDependencies(a);print('$_kTag#didChangeDependencies, ${widget.uniqueId}, $this');

  }

  @override
  void initState(a) {
    super.initState(a); PageVisibilityBinding.instance.addObserver(this, ModalRoute.of(context));
   print('$_kTag#initState, ${widget.uniqueId}, $this');
  }

  @override
  void dispose(a) {
    PageVisibilityBinding.instance.removeObserver(this);
    print('$_kTag#dispose, ${widget.uniqueId}, $this');
    super.dispose(a); } @override
  void onForeground(a) {
    print('$_kTag#onForeground, ${widget.uniqueId}, $this');
  }

  @override
  void onBackground(a) {
    print('$_kTag#onBackground, ${widget.uniqueId}, $this');
  }

  @override
  void onAppear(ChangeReason reason) {
    print('$_kTag#onAppear, ${widget.uniqueId}, $reason, $this');
  }

  void onDisappear(ChangeReason reason) {
    print('$_kTag#onDisappear, ${widget.uniqueId}, $reason, $this');
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('tab_example'),
      ),
      body: SingleChildScrollView(
          physics: BouncingScrollPhysics(),
          child: Container(
              child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: <Widget>[
              Container(
                margin: const EdgeInsets.only(top: 80.0),
                child: Text(
                  widget.messages,
                  style: TextStyle(fontSize: 28.0, color: Colors.blue),
                ),
                alignment: AlignmentDirectional.center,
              ),
              Container(
                margin: const EdgeInsets.only(top: 32.0),
                child: Text(
                  widget.uniqueId,
                  style: TextStyle(fontSize: 22.0, color: Colors.red),
                ),
                alignment: AlignmentDirectional.center,
              ),
              InkWell(
                child: Container(
                    padding: const EdgeInsets.all(8.0),
                    margin: const EdgeInsets.all(30.0),
                    color: Colors.yellow,
                    child: Text(
                      'open flutter page',
                      style: TextStyle(fontSize: 22.0, color: Colors.black),
                    )),
                onTap: () => BoostNavigator.of().push("flutterPage",
                    arguments: <String, String>{'from': widget.uniqueId}),
              )
              Container(
                height: 300,
                width: 200,
                child: Text(
                  "', style: TextStyle (fontSize: 22.0, color: Colors. Black),),),)))); }}Copy the code

Then, run the project to jump from the native page to the Flutter page, as shown below.

IOS 2.2 integration

As with the Android integration steps, open the native iOS project using Xcode and initialize Flutter Boost in the iOS AppDelegate file, as shown below.

@interface AppDelegate (a)

@end

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
  MyFlutterBoostDelegate* delegate=[[MyFlutterBoostDelegate alloc ] init];
    [[FlutterBoost instance] setup:application delegate:delegate callback:^(FlutterEngine *engine) {
    } ];

    return YES;
}
@end
Copy the code

Here is the code for the custom FlutterBoostDelegate, as shown below.

@interface MyFlutterBoostDelegate : NSObject<FlutterBoostDelegate>
@property (nonatomic,strong) UINavigationController *navigationController;
@end

@implementation MyFlutterBoostDelegate

- (void) pushNativeRoute:(FBCommonParams*) params{
    BOOL animated = [params.arguments[@"animated"] boolValue];
    BOOL present= [params.arguments[@"present"] boolValue];
    UIViewControllerDemo *nvc = [[UIViewControllerDemo alloc] initWithNibName:@"UIViewControllerDemo" bundle:[NSBundle mainBundle]];
    if(present){
        [self.navigationController presentViewController:nvc animated:animated completion:^{
        }];
    }else{ [self.navigationController pushViewController:nvc animated:animated]; }} - (void) pushFlutterRoute:(FBCommonParams*)params {

    FlutterEngine* engine =  [[FlutterBoost instance ] getEngine];
    engine.viewController = nil;

    FBFlutterViewContainer *vc = FBFlutterViewContainer.new ;

    [vc setName:params.pageName params:params.arguments];

    BOOL animated = [params.arguments[@"animated"] boolValue];
    BOOL present= [params.arguments[@"present"] boolValue];
    if(present){
        [self.navigationController presentViewController:vc animated:animated completion:^{
        }];
    }else{ [self.navigationController pushViewController:vc animated:animated]; }} - (void) popRoute:(FBCommonParams*)params
         result:(NSDictionary *)result{

    FBFlutterViewContainer *vc = (id)self.navigationController.presentedViewController;

    if([vc isKindOfClass:FBFlutterViewContainer.class] && [vc.uniqueIDString isEqual: params.uniqueId]){
        [vc dismissViewControllerAnimated:YES completion:^{}];
    }else{
        [self.navigationController popViewControllerAnimated:YES];
    }

}

@end
Copy the code

If you want to open or close the Flutter page in native iOS code, you can use the following method.

[[FlutterBoost instance] open:@"flutterPage" arguments:@{@"animated":@(YES)}  ];
[[FlutterBoost instance] open:@"secondStateful" arguments:@{@"present":@(YES)}];
Copy the code

3. Boost the structure of Flutter

For hybrid projects, the native end and the Flutter end define the page differently. For a native, a page usually refers to a ViewController or Activity, and for a Flutter, a page usually refers to a Flutter component. What the FlutterBoost framework does is unify the concept of a page in a hybrid project, or weaken the concept of a container page for a Flutter component. In other words, when a native page exists, FlutteBoost ensures that a corresponding container page for a Flutter exists.

The FlutterBoost framework is actually the native container that drives the Flutter page container through messages, so that the native container can synchronize with the Flutter container. The contents of the Flutter rendered by the native container are driven by the native container. Here is a schematic of the structure of a Flutter Boost.

As you can see, the Flutter Boost plugin is divided into platforms and Dart ends, connected via Message Channel. The platform side provides the configuration and management of the Flutter engine, creation/destruction of Native containers, notification of page visibility changes, and interface to open/close Flutter pages. In addition to providing a page navigation interface similar to native Navigator, the Dart side is responsible for the route management of Flutter pages.

In general, it is based on sharing the same engine that makes FlutterBoost framework effectively solve the problem of multiple engines. To put it simply, FlutterBoost introduces the concept of a container in Dart. When there are multiple Flutter pages, FlutterBoost does not need to maintain the existing page using the stack structure, but uses the form of flat key-value pair mapping to maintain all the current pages, and each page has a unique ID

4. FlutterBoost3.0 update

4.1 Do not hack the engine

In order to solve the problems caused by the reuse of the official engine, FlutterBoost2.0 copies some of the embed code of the Flutter engine for modification, which makes the upgrade cost very high. And FlutterBoost3.0 adopt the way of inheritance extension FlutterActivity/FlutterFragment the ability of components, and by at the right time to Dart side appIsResumed messages sent to solving engine reuse the page life cycle events disorder lead to stuck problem, FlutterBoost 3.0 is also compatible with Flutter 2.0, the latest official release.

4.2 No distinction between Androidx and Support branches

FlutterBoost2.0 FlutterActivityAndFragmentDelegate was achieved by themselves. The Host interface to extend FlutterActivity and FlutterFragment ability, GetLifecycle is the interface that must be implemented, which leads to a dependency on androidx. This is why the implementation of FlutterBoostView is not included in the FlutterBoost3.0 plug-in. By inheritance and FlutterBoost3.0 ability to expand FlutterActivity/FlutterFragment extra income is, can not rely on androidx.

4.3 Unified dual-end design and interface

Many Flutter developers know only one end, either Android or IOS, but they need access to both ends, so this can reduce their learning costs and access costs. In the design of FlutterBoost3.0, both Android and IOS have made alignment, especially the parameter level alignment on the interface.

4.4 The “Open the Flutter page without opening the container” scenario is supported

Inside the Flutter module, a Flutter page can jump to a Flutter page without opening the Flutter container. This saves memory. In FlutterBoost3.0, the difference between open and unopen containers is simply whether the withContainer parameter is true on the user interface.

InkWell(
  child: Container(
      color: Colors.yellow,
      child: Text(
        'Open external Routes',
        style: TextStyle(fontSize: 22.0, color: Colors.black),
      )),
  onTap: () => BoostNavigator.of().push("flutterPage",
      arguments: <String, String>{'from': widget.uniqueId}),
),
InkWell(
  child: Container(
      color: Colors.yellow,
      child: Text(
        'Open internal routing',
        style: TextStyle(fontSize: 22.0, color: Colors.black),
      )),
  onTap: () => BoostNavigator.of().push("flutterPage",
      withContainer: true,
      arguments: <String, String>{'from': widget.uniqueId}),
)
Copy the code

4.5 Accurate notification of life cycle

On FlutterBoost2.0, each page receives a page lifecycle notification, whereas FlutterBoost3.0 only notifies pages whose visibility has actually changed, and the interface is more in line with flutter design.

4.6 other Issue

In addition to the above features, Flutter Boost 3.0 also addresses the following issues:

  • The transfer of parameters after the page is closed was previously only supported by iOS, but not Android. Now it is implemented on dart side, supported by both iOS and Android.
  • Fixed Android status bar font and color issues.
  • Fixed page rollback willPopScope not working.
  • Resolved an issue where pages not at the top of the stack would receive lifecycle callbacks
  • Solve multiple setState performance problems.
  • Demo of Various Framgent access modes is provided to facilitate TAB access.
  • Lifecycle callback code can be accessed with user code, easier to use.
  • Overall simplified access costs, including dart side, Android side and ios
  • Enriched demo, including basic scenarios, easy user access and test regression