Introduction: Those who have done complete projects should know that with the development of business, the operation requirements of APP will be more and more (for example, the UI of the page will be dynamically changed according to the operation activities). This requires our APP to meet the dynamic operation requirements of the market as much as possible. Through this article, you will learn: 1. 2. For small and medium-sized teams, how to realize the dynamic requirements of APP at the lowest cost and most efficiently?

The common ways and implementation principles of dynamic

First of all, what is dynamic? That is, independent of the program installation package, can be dynamic real-time page update technology. The following is a list of common methods and principles:

  • Usually you think of itwebviewThat’s really the most common way, but it’s also the least stable way of doing dynamics; The experience of Webview is poor, and a large number of devices need to be compatible.
  • Gpl-based Native enhancements. GPL is a general-purpose programming language, such as Dart, JavaScript, etc., used to make Native features more dynamic. This is a gPL-based Native enhancement: the operator dynamically changes js files online, and the Flutter application dynamically renders them by pulling updates from the network.
  • Dsl-based Native enhancement. DSLS are domain specific languages that are specifically designed to solve tasks in specific scenarios, such as XML, JSON, CSS, and HTML. Pages are dynamically configured through parsing protocols by generating simple DSL language files.

Looking at the dynamic ecology of Flutter as a whole, there is no mature open source framework on the market at present, only major Domestic Internet companies open source one after another, but all of them are in urgent need of maintenance. The current mainstream frameworks include:

  1. Tencent open source MXFlutter
  2. 58 city open source Flutter Fair
  3. Alibaba’s open source North Sea Karken

I’ll also introduce two more common methods:

  1. The webview enhancement[Embedded into Tencent X5 kernel and upgraded the model]
  2. UI library templating

Dynamic architecture comparison of major factories

Flutter Fair

Fair is 58.Com’s dynamic framework for Flutter. It dynamically updates Widget Tree and State by automatically converting JSON configuration and native Dart source files using the Fair Compiler tool.

Used to introduce

The Fair plugin on pub is not maintained officially, we need to go to GitHub fork source code to write the demo. 58Fair prepares a configured JSON file and simply calls FairWidget to pass in the file path to display it. The need to be dynamic is nothing more than to put the JSON configuration file online, and then the FairWidget pulls it down again each time to be dynamic.

/// The basic use
return Container(
    alignment: Alignment.centerLeft,
    color: Colors.white,
    constraints: BoxConstraints(minHeight: 80),
    child: FairWidget(
      name: item.id,
      path: 'assets/bundle/sample.json',
      data: {"fairProps": json.encode({'detail': details})},
    ),
);
Copy the code

Following the source code, we can see that when we pass a file path that starts with HTTP, it will be pulled over the network

void _reload() {
  var name = state2key;
  varpath = widget.path ?? _fairApp.pathOfBundle(widget.name); bundleType = widget.path ! =null && widget.path.startsWith('http')?'Http'
      : 'Asset';
  parse(context, page: name, url: path, data: widget.data).then((value) {
    if(mounted && value ! =null) { setState(() => _child = value); }}); }/// Decoder → Bundle is then accessed layer by layer through parse()_provider, look at the onLoad method
@override
Future<Map> onLoad(String path, FairDecoder decoder,
    {bool cache = true.Map<String.String> h}) {
  bool isFlexBuffer;
  if (path.endsWith(FLEX)) {
    isFlexBuffer = true;
  } else if (path.endsWith(JSON)) {
    isFlexBuffer = false;
  } else {
    throw ArgumentError(
        'unknown format, please use either $JSON or $FLEX; \n$path');
  }
  if (path.startsWith('http')) {
    return _http(path, isFlexBuffer, headers: h, decode: decoder);
  }
  return _asset(path, isFlexBuffer, cache: cache, decode: decoder);
}

/// Focus on parsing methods that start with HTTP, using HTTP kulls
Future<Map> _http(String url, bool isFlexBuffer,
    {Map<String.String> headers, FairDecoder decode}) async {
  var start = DateTime.now().millisecondsSinceEpoch;
  var response = await client.get(url, headers: headers);
  var end = DateTime.now().millisecondsSinceEpoch;
  if(response.statusCode ! =200) {
    throw FlutterError('code=${response.statusCode}, unable to load : $url');
  }
  var data = response.bodyBytes;
  if (data == null) {
    throw FlutterError('bodyBytes=null, unable to load : $url');
  }
  Map map;
  map = await decode.decode(data, isFlexBuffer);
  var end2 = DateTime.now().millisecondsSinceEpoch;
  log('[Fair] load $url, time: ${end - start} ms, json parsing time: ${end2 - end} ms');
  return map;
}
Copy the code

Look at the dependencies, in fact, are very old, obviously not enough maintenance; There are also restrictions on the version of The Flutter. For every version of the Flutter,58Fair officials will probably need to adapt it.

  fair_annotation:
    path: ../annotation
  fair_version:
    path: ../flutter_version/flutter_2_5_0

  flat_buffers: ^ 1.12.0
  url_launcher: ^ 5.7.2
  http: ^ 0.12.2
Copy the code

Finally how to write JSON configuration file, must have a set of protocol, follow the official documentation Api can be written. Those familiar with Flutter should understand the following code sample.

{
  "className": "Center"."na": {
    "child": {
      "className": "Container"."na": {
        "child": {
          "className": "Text"."pa": [
            "Nested dynamic Components"]."na": {
            "style": {
              "className": "TextStyle"."na": {
                "fontSize": 30."color": "#(Colors.yellow)"}}}},"alignment": "#(Alignment.center)"."margin": {
          "className": "EdgeInsets.only"."na": {
            "top": 30."bottom": 30}},"color": "#(Colors.redAccent)"."width": 300."height": 300}}}}Copy the code

The pros and cons analysis

  1. The benefits of Fair: Simple to use and stable performance;
  2. The disadvantages are obvious:
  • Using JSON to configure the UI is doomed to not support logic;
  • Flutter has too many widgets, and Fair currently only matches a limited static UI.
  • Out of the Dart ecosystem, the UI is written in JSON…… ;
  • teamMaintenance is very limitedMany plugins have not been updated and pub has not been updated. But it isA common problem with all Flutter dynamic open source frameworks😭 】

MxFlutter

MxFlutter also has limited maintenance and pub is not the latest version. GitHub address has also been changed, please see the latest oneGithub.com/Tencent/mxf…

MxFlutter writes Dart in JavaScript, as wellThe dynamic effect can be achieved by loading js files on the line and converting and displaying them at runtime through the engine.The official release of version 0.7.0 includes TypeScript, introduces the NPM ecosystem, optimizes the cost of JS development, and brings the ecosystem closer to the front end.

Unfortunately, when comparing the solutions of various manufacturers, it was found that the cost performance of the MxFlutter was extremely low.The cost of learning is also high, and the JS ecology is abandoned, and the Dart ecology is abandoned. Therefore, FOR MxFlutter, I only made a simple analysis of the implementation principle without in-depth study.

Used to introduce

  • Initialize engine
String jsBundlePath = await _copyBizBundelZipToMXPath();
if(jsBundlePath ! =null) {
  // Start MXFlutter and load the JS library.
  MXJSFlutter.runJSApp(jsAppPath: jsBundlePath);
}
Copy the code
  • Pass in the JS script via MXJSPageWidget and it will be parsed and displayed. The usual use of MxFlutter is to present an entire page written using the MxFlutter framework
Navigator.push(
  context,
  MaterialPageRoute(
    builder: (context) => MXJSPageWidget(
        jsWidgetName: "mxflutter-ts-demo",
        flutterPushParams: {
          "widgetName": "WidgetExamplesPage"}),),);Copy the code
  • Looking at the interface definition of MxJsFlutter, we can see that there are many protocols defined, which is bound to increase the learning cost of JS, and the engine dependence on mxFlutter is very high. And whether his team has the ability to hold the pit here, is to consider carefully.
abstract class MXJSFlutter {
  static MXJSFlutter _instance;
  static String _localJSAppPath;
  static String get localJSAppPath => _localJSAppPath;

  /// Gets the external interface class MXJSFlutter.
  /// Most of the interfaces to MXFlutter are invoked via MXJSFlutter.
  static MXJSFlutter getInstance() {
    if (_instance == null) {
      _instance = _MXJSFlutter();
    }
    return _instance;
  }

  /// Start the JSApp with the Flutter code. This can be used to display the Flutter page first and then Push the route to the JS page.
  /// After launching JSApp, JS code is executed. JS code can either actively invoke Flutter to display its own pages, or accept Flutter instructions to display the corresponding pages.
  ///
  /// @param jsAppPath jsApp root Path, JS Business code is placed in a folder. JsAppPath or jsAppAssetsKey Choose one from two scenarios.
  /// Yaml to set jsAppPath to the mxflutter folder of the lib, ios folder_js_Bundle/folder.
  /// @param jsExceptionHandler JS exception callback. The method parameters are described in MXJSExceptionHandler.
  /// @param debugBizJSPath can only be used in iOS emulator!! Local js directory to place XXX /bundle-xxx.js files directly, without packing bizbundle.zip. After this parameter is used, jsAppPath does not take effect.
  /// @returns Future
  /// @throws Error if Path error
  ///
  static Future runJSApp(
      {String jsAppPath = ' '.String jsAppAssetsKey = defaultJSBundleAssetsKey,
      MXJSExceptionHandler jsExceptionHandler,
      String debugJSBundlePath = ' '}) async {
    WidgetsFlutterBinding.ensureInitialized();
    MXJSFlutter.getInstance();

    if(jsAppPath == null || jsAppPath.isEmpty){
      jsAppPath  = await defaultJSAppUpdatePath();
    }

    // Check whether the main.zip package needs to be copied.
    MXBundleZipManager bundleManager = MXBundleZipManager(jsAppPath: jsAppPath);
    MXBundleZipCheckResult result = await bundleManager.checkNeedCopyMainZip();

    if(! result.success) { MXJSLog.log('MXJSFlutter.runJSApp: checkAppBundleZip failed: ${result.errorMessage}');

      // Engine initialization success callback
      MXJSFlutter.getInstance().jsEngineInitCompletionHandler(
          {'success': result.success, 'errorMessage': result.errorMessage});

      return;
    }

    // If the debugJSBundlePath is not empty, the js file in this directory is run.
    String realJSAppPath = jsAppPath;
    if(debugJSBundlePath ! =null &&
        debugJSBundlePath.isNotEmpty &&
        await canDefineDebugJSBundlePath()) {
      realJSAppPath = debugJSBundlePath;
    }

    _localJSAppPath = realJSAppPath;

    // Load main.js.
    _callNativeRunJSApp(
        jsAppPath: realJSAppPath, jsExceptionHandler: jsExceptionHandler);
  }

  /// The hot update directory of the default JSBundle, used to store downloaded JSBundle files
  static Future<String> defaultJSAppUpdatePath() async {
    // If the service does not specify a directory, the default directory is used
    return await Utils.findLocalPath() +
        Platform.pathSeparator +
        mxJSAPPDefaultAssetsKey;
  }

  /// Whether the debugJSBundlePath can be defined
  static Future<bool> canDefineDebugJSBundlePath() async {
    // Currently only scenarios are supported: 1) Debug the iOS emulator environment
    if (kDebugMode && Platform.isIOS) {
      DeviceInfoPlugin deviceInfoPlugin = DeviceInfoPlugin();
      IosDeviceInfo deviceData = await deviceInfoPlugin.iosInfo;
      return! deviceData.isPhysicalDevice; }else {
      return false; }}static _callNativeRunJSApp(
      {String jsAppPath = "", MXJSExceptionHandler jsExceptionHandler}) {
    Map<String.dynamic> args = {"jsAppPath": jsAppPath};

    // Set JS Exception Handler.
    MXPlatformChannel.getInstance().setJSExceptionHandler((arguments) {
      // If arguments['jsFileType'] is 0 then the JS engine success callback is executed.
      if (arguments is Map && arguments['jsFileType'] = =0) {
        MXJSFlutter.getInstance().jsEngineInitCompletionHandler(
            {'success': false.'errorMessage': arguments['errorMsg']});
      }

      // Callback to the service side.
      if(jsExceptionHandler ! =null) { jsExceptionHandler(arguments); }});// Initialize MXFFICallbackManager.
    MXFFICallbackManager.getInstance();
    
    args["flutterAppEnvironmentInfo"] = flutterAppEnvironmentInfo;
    MXPlatformChannel.getInstance().invokeMethod("callNativeRunJSApp", args);
  }

  ///Write a JS page from Flutter Push
  ///@ param widgetName: "widgetName", in the main. Js MyApp: : createJSWidgetWithName function
  ///MyApp: : createJSWidgetWithName to create the corresponding JSWidget by widgetName
  /// In general, you should use the MXJSPageWidget wrapper class of the higher level API to display JS widgets. See the usage of MXJSPageWidget
  dynamic navigatorPushWithName(
      String widgetName, Key widgetKey, Map flutterPushParams,
      {String bizPath});

  /// Set up the processor to customize the Loading widget when the JS page loads.
  void setJSWidgetLoadingHandler(MXWidgetBuildHandler handler);

  /// Set up the handler to customize the Error Widget when the JS page loads an Error.
  void setJSWidgetBuildErrorHandler(MXWidgetBuildHandler handler);

  /// JS engine initialization end callback.
  void jsEngineInitCompletionHandler(dynamic arguments);

  /// Check whether the JS engine is initialized.
  bool isJSEngineInit();

  /// Set JS engine initialized.
  void setJSEngineInit();

  /// JS engine initialization result.
  Map<String.dynamic> jsEngineInitResult();

  /// Recreate the MXJSFlutter, including the channels and properties.
  void resetup();

  /// The current flutterApp.
  MXJSFlutterApp get currentApp;
}
Copy the code

Karken

Karken is an open source ali high performance rendering engine based on W3C standards. It is also the library with the highest level of maintenance within the framework of several big factories. See GitHub for details. The advantage of Karken is that it can be developed based on W3C, and it introduces NPM ecology to support development using Vue and React frameworks. General front-end personnel can develop, and the learning cost is very low.

Used to introduce

Pubspec is introduced and the url of the script is passed in directly using Widget Kraken.

kraken: ^ 0.9.0
Copy the code
Widget build(BuildContext context) {
  // We just need to maintain the JS script
  Kraken kraken = Kraken(
      bundleURL:
          'http://kraken.oss-cn-hangzhou.aliyuncs.com/demo/guide-styles.js');
  return Scaffold(
    appBar: PreferredSize(
      preferredSize: Size.fromHeight(40),
      child: AppBar(
        centerTitle: true,
        title: new Text(
          'Merchandise Details',
          style: Theme.of(context).textTheme.headline6,
        ),
      ),
    ),
    body: kraken,
  );
}
Copy the code

As you can see, the key lies in how we maintain JS files with dynamic operation content, which is the most competitive point of Karken compared to other frameworks. The official API is written in detail, based on W3C standards, and can be developed using mainstream frameworks such as Rax, Vue and React.

/ / / Vue code
<template>
  <div :style="style.home">
    demo
  </div>
</template>


<script>
  const style = {
    home: {
      display: 'flex'.position: 'relative'.flexDirection: 'column'.margin: '27vw 0vw 0vw'.padding: '0vw'.minWidth: '0vw'.alignItems: 'center',}};export default {
    name: 'App'.data() {
      return{ style, }; }};</script>
Copy the code

Kraken’s disadvantage is that it does not support CSS styles, making Vue development a relatively generic experience. But in general, it has been very good, the official maintenance intensity is large, to meet the front-end ecology, easy to use, is a good choice for dynamic technology.

Webview enhancement optimization

Almost all mobile applications use webView as h5 container. Generate H5 through the configuration of the operating platform, and the APP can display it directly. Unfortunately, there are a lot of problems with webView experience, stability/compatibility. Experience loading process blank screen, loading, error state cannot be defined; IOS compatibility is good, browser kernel is WKWebView, but Android devices are diverse, browser kernel is uneven, so there are often problems in compatibility. To solve the above problems, we made the following solutions based on the official plugin webview_FLUTTER:

  • Modify the WebView plug-in to a configurable transparent background and remove the loading bar. The Flutter layer develops the enhanced container of WebView, which can define loading and loading failure views, and achieve the loading effect basically consistent with APP
  • In terms of stability, we adopt a unified method to implant X5 kernel

Why the X5 kernel?

Currently, there are few open source browser kernel SDKS, including ChromeView, Crosswalk and TbsX5.

  1. Open source ChromeView based on The Chromium kernel is basically not maintained at present. Another problem is that the compiled dynamic library is too large, arm-29M, x86-38m, which is undoubtedly a big problem for the size of app. Hence the abandonment of Chromium-based ChromeView.
  2. Crosswalk is also based on The Chromium kernel, which also has the size problem of the app mentioned above, so it is also abandoned.
  3. TbsX5Based on Google Blink kernel, ecology in China is very mature, as long as the mobile phone equipped with wechat, all support X5. X5 provides two integration solutions:
    • X5 kernel only sharing wechat Q space (for share)
    • Download the X5 kernel with Download

Optimize the experience

  • Modify webview_flutter to have a configurable transparent background. See my previous article # How do I add a transparent background to the Flutter webview? Final business layer code:
WebView(
  initialUrl: 'https://www.baidu.com',
  transparentBackground: true
)
Copy the code
  • Build the WebView container. The WebView background is handled transparently through the Stack layout, as well as listeningonProgressCallback, endows webView container loading, loading failure effect, so that the user experience is similar to the native application.
/// We use a flutter_bloc Performs state management
Stack(
  alignment: Alignment.center,
  children: [
    WebView(
      transparentBackground: widget.transparentBackground,
      onProgress: (int progress) {
        if (progress >= 100) { context.read<WebViewContainerCubit>().loadSuccess(progress); } }, onWebResourceError: (error) { context.read<WebViewContainerCubit>().loadError(); },),if (state is WebViewLoading)
      Center(
        child: widget.loadingView ?? const LoadingView(),
      ),
  ],
)
Copy the code

Look at the Bloc layer, it’s a very simple state switch.

/// Cubit
class WebViewContainerCubit extends Cubit<WebViewContainerState> {
  WebViewContainerCubit() : super(WebViewLoading());

  loadSuccess(int progress) {
    if (state != WebViewLoadSuccess()) {
      emit(WebViewLoadSuccess());
    }
  }

  loadError() {
    emit(WebViewLoadError());
  }
}

/// State
abstract class WebViewContainerState {}

class WebViewLoading extends WebViewContainerState {}

class WebViewLoadSuccess extends WebViewContainerState {}

class WebViewLoadError extends WebViewContainerState {}
Copy the code

Embed X5 kernel

There are also a few Webview for X5 wheels available on the pub, but they were in disrepair, didn’t have ongoing maintenance, and didn’t even have the Null safely support.

So we continue to expand webview_FLUTTER library, create webview_flutter_android_x5 module, introduce X5 SDK, focus on the officialWebview_flutter_android related functions and Api replacement development. It also provides an Api to the business layer and lets the caller decide whether to enable the X5 kernel.

The implantation of X5 requires a certain native basis, here is not too much on the source code, if I have the opportunity to directly open source a library, the transparent background and X5 kernel together.

UI component library templating

In this way, some holes are embedded through UI design, and the operation end determines how to display THE UI by matching the existing components stored in the interface and pulling the background service each time. This is a very general approach, less dynamic, and requires the design of the possible UI first. But the most reliable, but in the development of the NEED to do a good job of UI library encapsulation, protocol customization, at the same time to pay attention to the degradation of processing, if the network can not pull the background data, how to do the page display, this should be handled well.

summarizing

Looking back at the diagram below, there is no doubt that Kraken is most useful in production in terms of frames, but here’s what I want to sayThese frameworks are immature, and in order for them to be used in production, the team must be able to participate in open-source, pothole filling.By the way, Tencent QQ Music is preparing open source Kant, which can also be paid attention to. The modification based on Kraken has been applied in internal production, which is worth looking forward to. While webView enhancement, UI component templating is a relatively reliable way, the general team has the ability to maintain. In both cases, the people running the platform don’t have to learn new apis, and the development cost of managing the configuration dynamic content in the background is much lower.