Due to the early development of some businesses and the large proportion of Web programs in the long-term App, there is a lot of accumulation of Web-related bridges. When developing Flutter, how to reuse the end capabilities has become the focus. Therefore, the Flutter Bridge integration solution in the design can seamlessly integrate with the existing bridge solutions on the native side, converting the default bridge into a flutter channel for use. This solution will also become one of the core parts of the future Flutter cross-end integration solution.

The design of the cross-end trans-bridge consists of two main components, the Flutter Client, which supports automatic codegen generation of bridge call code fragments on the Flutter side, and the trans-bridge, which provides higher-level abstraction for the Native Bridge on the native side. We will describe the use of these two parts and the principles of the parts below.

Flutter Client

Single Bridge Channel Implementation

Based on the analysis of the Flutter Platform Channel, we can conclude that the communication implemented by Flutter on the Dart and Native sides is full-duplex asynchronous communication. Therefore, in principle, we use a single MethodChannel name for integrated transmission and communication of multiple bridges, which will not cause blocking problems in itself, and verify through pressure measurement experiments that the split transmission of the same large text (1M text in multiple asynchronous logical transmissions and a single channel transmission) will not be faster, so we can pass it through a single Bridge Channel and transfer it to the Bridge side.

Bridge Annotation

Take a simple show toast method as an example:

    @Bridge('Toast')
    mixin Toast on BridgeModule {
     @BridgeMethod()
     Future> showToast(
       {@required String title,
       @required int duration,
       Map extra});
     @BridgeMethod()
     Future> hideToast();
    }

By writing a class that uses Bridge to describe Toast, and in which showToast and hideToast corresponding to Web Bridge are implemented according to parameter needs and useBridgeMethodThese Annotations have some parameters that can control the code generation of some specific CodeGen Processor, only the default situation is used here.

  • Code generation:

    flutter packages pub run build_runner build
    
  • The result is:

    // GENERATED CODE - DO NOT MODIFY BY HAND
    part of 'toast.dart';
    // **************************************************************************
    // BridgeGenerator
    // **************************************************************************
    class ToastBridge extends BridgeModule with Toast {
     ToastBridge({BridgeClient client}) : super(client: client);
     Future showToast({
      @required String title,
      @required int duration,
      Map extra,
     }) {
      var _$params = {
       'title': title,
       'duration': duration,
       'extra': extra,
      };
      var _$result = client.getData('TTRToast.showToast', params: _$params);
      return _$result;
     }
     Future hideToast() {
      var _$result = client.getData('TTRToast.hideToast');
      return _$result;
     }
    }
    

Generates the result of automatic splicing parameters, and according to the Web side naming, parameter rules will call Bridge Client for transmission.

Bridge Client Support

    */// Bridge channel.*
    final MethodChannel _channel;
    */// Default channel name.*
    static const String *defaultChannelName* =
      'com.example.hybrid.bridge-flutter';
    */// Error emitter.*
    final StreamController _errorEmitter =
      new StreamController.broadcast(sync: false);
    BridgeClient({
     String channelName = *defaultChannelName*,
    }) : _channel = MethodChannel(channelName, JSONMethodCodec()) {
     _channel.setMethodCallHandler(_onEvent);
    }

The Bridge Client side uses a unified Method Channel for development, which is not only compatible with the existing Bridge implemented by the Web and React Native side, but also avoids the inconvenient multi-Method Channel scheme at this stage. Due to the full duplex and asynchronous call scheme implementation of Method Channel, using a single Method Channel will not cause additional performance problems.

You can write the following code directly by calling on the Dart side:

    client.ui.toast('xxxxx')

Trans Bridge Implementation

Flutter’s Plugins code organization is not yet mature compared to the Method organization of various Bridge applications. Of course, Flutter is also gradually promoting the development of more convenient cross-end methods to enable better hybrid engineering.

Trans Bridge implements a set of bridge implementations with a higher level of abstraction in dual-ended Android and iOS. Through the extraction of the intermediate protocol layer, the information sent from the dual-ended can be trans to different specific jsbridge, react native bridge, flutter bridge, and native support. Whether it contains various bridge support libraries inside and outside the company, or js-bridge or react native-bridge, but if you need to implement cross-end support from Flutter to Native, you must design an implementation plan to develop and support multiple bridges, and use this to continue the development of end capabilities. At present, this system supports trans-support access for PGC Hybrid and IES JSBridge.

Structure

Only the Trans Bridge scheme for accessing Flutter is described here. The structure contains the following modules:

- fluttermk
- transbridge-core
- transbridge-hybrid
- transbridge-jsbridge2
- transbridge-flutterimpl
- ... other bridge impl.
  • Fluttermk: At present, flutter does not have an expedient solution for product-dependent implementations. It provides some required flutter-sdk mock interfaces on the pule-native side, and provides corresponding Adapter bindings in regular flutter-plugins. Currently, only MethodChannel related bindings are used.

At present, Flutter’s hybrid engineering implementation should have a corresponding mvn library, hoping to better develop product-dependent solutions and reduce the implementation of similar protocols.

  • Transbridge-core: provides abstract connection from Flutter Method Channel to generalized JsBridge Call, provides simple Call, Result, Host, Context and other related operations, leaving enough interfaces with specific bridge implementations for access.

  • Transbridge-xxx: Flutter - Web Bridge JsBridge for different specific implementation of the difference, leaving enough available send event and call method interface for automatic binding, the current completion of the hybird jsbridge, jsbridge 2 access.

  • Transbridge-flutterimpl: Bridge support without specifying it, no spec coupling with related platforms such as js, react native or flutter, provides a specific call and implementation of the interface described by the core module, and includes binding access to Flutter spec outside here.

Bridge Usage

When using Trans Bridge, it is very similar to the implementation of most jsbridges. For example, the above Flutter Client-side example can be implemented on the native side as:

    public class VLUIBridge extends BridgeMethods.ComposeMethod {
      @SubMethod
      public Single showToast(IBridgeContext context, String name, JsonObject params) {
        Toast.*makeText*(getContext(), params.get("text").getAsString(), Toast.*LENGTH_SHORT*).show();
        return BridgeResult.*createSingleSuccessBridgeResult*();
      }
    }

Bridge Core Design

├── AbsBridgetMethodCallHandler.java
├── IBridgeAuth.java
├── IBridgeCall.java
├── IBridgeContext.java
├── IBridgeHost.java
├── IBridgeMethod.java
└── IBridgeResult.java

Bridge Core includes only the following interfaces, and the default Stub implementation of each interface to avoid interface upgrade conflicts:

  • IBridgeCall provides a usage description for a specific Bridge

  • AbsBridgeMethodCallHandler describes calls from FlutterMK to an IBridgeCall abstract description

  • IBridgeContext describes the Context abstractions included in the runtime

  • IBridgeMethod provides abstraction for concrete Bridge Method

  • IBridgeResult provides a description of the results for Bridge

  • IBridgeHost provides an abstraction of the Bridge call host delegate

  • IBridgeAuth provides authentication abstraction for BridgeScope

Here are some representative abstract descriptions:

BridgeCall

    public interface IBridgeCall {
      void call(@NonNull String name, @NonNull JsonObject params, @NonNull View view, @NonNull MethodChannelMk.ResultMk result);
    }

Bridge Call can be abstracted as a method signature description of (name, params, view, result ) => Unit. It can be considered that an abstracted bridge only needs to provide:

  • Name Method name

  • Params parameter information

  • View for relevant spec information

  • FlutterMk return receipt held as a result

It can be described as an abstraction of a Bridge. A spec bridge can be called and supported by the Bridge Method Call Handler through the description of the IBridgeCall interface.

BridgeMethodCallHandler

      @Override
      public void onMethodCall(@NonNull MethodCallMk call, @NonNull MethodChannelMk.ResultMk result) {
        if (call.name() == null || call.name().length() == 0) {
          result.notImplemented();
          return;
        }
        JsonObject args = (JsonObject) call.arguments();
        bridge.call(call.name(), args, host, result);
      }

MethodCallHandler uses FlutterMk’s Flutter Protocol to forward the Flutter-side Method Channel to an abstract description of an IBridgeCall, which can be considered as the calling process of a specific Bridge. From this step, Flutter’s actual Method Channel is converted to a specific Native-side Bridge implementation.

BridgeMethod

    public interface IBridgeMethod {
      EnumSet getType();
      Single call(IBridgeContext context, String name, JsonObject params);
    }

The abstraction of BridgeMethod only provides MethodTypes saved through EnumSet, which we then implement for different MethodTypes to handle various permission processing, async or sync call processes separately. And the main methodcallThat is, the above IBridgeCall call scheme for Bridge support.

IBridgeMethod also supports the specialization of many types of methods, such as Public, Private, Protected attributes for management Bridge permission control and ASync, Sync, Compose and other attributes can be used according to needs. And in flutter-impl support implements a simple registration mode for various combination types of Annotation.

Bridge Spec Impl

The Spec Impl here specifies the Spec specialization of the Trans-Bridge to a specific JSBridge, which is different from the full set of JSBridge implementations implemented by flutter-impl. On top of a specific Bridge transformation, more is how to convert the abstract methods and invocation methods described by Bridge-Core to a corresponding Bridge implementation, or by wrapping the abstract method into the method package of the specific Bridge. Currently, it has been implemented to connect to the other bridges.

For example, the above figure is a concrete implementation of converting this straddle bridge solution to jsbridge2. Flutter Bridge Impl only realizes the relevant functions of Flutter Mk’s Protocol Adapter. The core JsBridge2Call describes how to abstract jsbridge2 to the implementation of IBridgeCall:

    public class Jsbridge2Call extends IBridgeCall.BridgeCallStub {
      private final JsBridge2 bridge;
      private final FlutterBridgeImpl actualBridge;
      public Jsbridge2Call(MethodChannelMk channelMk) {
        Environment env = JsBridge2.*create*().setCustomBridge(new FlutterBridgeImpl(channelMk));
        this.actualBridge = (FlutterBridgeImpl) env.getCustomBridge();
        this.bridge = env.build();
      }
      @Override
      public void call(@NonNull String name, @NonNull JsonObject params, @NonNull View view, @NonNull MethodChannelMk.ResultMk result) {
        Js2JavaCall call = Js2JavaCall.*builder*()
            .setMethodName(name)
            .setCallbackId("invalid")
            .setType("invalid")
            .setVersion("1.0")
            .setNamespace("host")
            .setParams(params.toString())
            .build();
        this.actualBridge.setResultMk(call,result);
        this.actualBridge.invokeMethod(call);
      }
    }

This communication with Amano opened up the internal bridge call method, and constructed the parameters of the call parameters through the built-in data class Js2JavaCall of jsbridge2. The specific invoke process was later implemented in the implementation of FlutterBridgeImpl, invoke Js and invodeJsCallBack, but only forwarded part of the FlutterMk implementation Protocol.

    @Override
    protected void invokeJs(String params) {
      JsonObject obj = *gson*.fromJson(params, JsonObject.class);
      channelMk.invokeMethod(obj.get("__event_id").getAsString(), obj.getAsJsonObject("__params"));
    }
    @Override
    protected void invokeJsCallback(String data, @Nullable Js2JavaCall call) {
      if (call == null || resultMkMap.get(call) == null) {
        return;
      }
      JsonObject obj = *gson*.fromJson(data, JsonObject.class);
      int code = obj.get("code").getAsInt();
      String resultData = obj.get("__data").getAsString();
      MethodChannelMk.ResultMk resultMk = resultMkMap.get(call);
      // TODO: result value callback
      this.resultMkMap.remove(call);
    }

Flutter Impl

Flutter-impl is a concrete implementation of a description scheme in Bridge-Core. If there is no particularly strong demand for jsbridge support on the end, integrated cross-end projects will be directly connected to the implementation of Flutter-Impl. Most of the functions can be understood as a no-platform-spec bridge design, and we will only explain and describe some of the features in the introduction here.

The implementation of Transbridge provides the use of binding to the lifecycle (although it may not be used), provides the abstraction of Host Delegate, binds to the lifecycle of Activity and the cycle of View, and will automatically destroy according to the lifecycle:

    public void addModule(View view, final Object key, Object module, boolean isWeak) {
      int id = ViewIDMarker.*ensureBridgeId*(view);
      Map map = mModules.get(key);
      if (map == null) {
        map = new ConcurrentHashMap<>();
        mModules.put(key, map);
      }
      map.put(id, isWeak ? new WeakReference<>(module) : module);
      if (view != null) {
        view.addOnAttachStateChangeListener(...);
      }
    }

Among them, the View ID Marker is provided through the lock-free mode of CAS, that is, it can quickly add Tag to the bundled View so that it can be quickly bound and processed when calling. In the bound view, in addition to more convenient binding life cycle, it can also get more specific data and information based on the implementation of different platforms (such as FlutterView and ReactNativeView) according to the Platform View.

In the overall data model, it can be considered that the bridge-method is preloaded, and the corresponding Host Delegate only adds a temporary entry-pointer to the call bridge. However, after the life cycle is destructed, the entry-pointer of this temporary response cannot be called.

    public interface IBridgeHook extends IBridgeMethod {
      // *TODO:* *add others hook type.*
      enum HookType {
    //     PRE,
        *CONVERT*,
    //     POST
      }
      HookType hookType();
    }
    public interface IGhostHook {
      Single onGhostHooks(IBridgeContext context, String name, JsonObject params);
    }

On top of the overall process according to the Bridge-Core given Context, Result, Method, Auth specific implementation, and provides a variety of manual support business Hook, GhostMethod Hook redefinition, unified processing can not be invoked Ghost Method.

    for (final Method declaredMethod : clazz.getDeclaredMethods()) {
      SubMethod subMethod = declaredMethod.getAnnotation(SubMethod.class);
      if (subMethod == null) {
        continue;
      }
      this.register(name + '.' + declaredMethod.getName(), new BridgeMethods.PublicMethod() {
        @Override
        public Single call(IBridgeContext context, String name, JsonObject params) {
          try {
            return (Single) declaredMethod.invoke(method, context, name, params);
          } catch (IllegalAccessException | InvocationTargetException e) {
            e.printStackTrace();
          }
          return BridgeResult.*createSingleErrorBridgeResult*(name);
        }
      });

In terms of Method registration, different registration schemes are also provided for the specific implementation of various Method Types. For example, the calling scheme using Compose Method can use @SubMethod to describe multiple Methods to design a group of non-independent Bridge Methods that can share states. And through Call to Observable, it is also very easy to implement the scheme that Async or Sync methods are called by value throughout the process.

Summary

This article explains the design and implementation of Flutter’s cross-end Bridge solution at present and for a period of time. Mainly from how to design a cross-end infrastructure design, how to split the fine grain and business-friendly fine grain modules, and how to integrate with the existing Native infrastructure in the Flutter life cycle. This also reminds us that when we design the architecture, we should also take into account the historical process. The project structure and call process that are too different from the original architecture are easy to be inconsistent. How to think about more generic architecture design can improve the universality and maintainability of the project. How to make the project have a better development experience and business friendliness is also the direction that Flutter officials and our Flutter Infra will strive for in the future.