By Karl_wei
Link: Dynamic update of Flutter - Technical pre research - Nuggets
Foreword: small partners who have done complete projects should know that with the development of business, there will be more and more operational needs for apps (for example, dynamically changing the UI of the page according to operational activities). This requires our app to meet the dynamic needs of market operation as much as possible. Through this article, you will learn:
1. use and effect comparison of the dynamic scheme of flutter;
2. for small and medium-sized teams, how to realize the dynamic requirements of app s at the least cost and most efficiently.
Common ways and implementation principles of dynamic
First, what is dynamic? That is, the technology of dynamically updating pages in real time without relying on the program installation package.
Next, common methods and principles are listed:
- Generally, we all think of webview, which is indeed the most commonly used method, but also the most unstable method in dynamic; The webview experience 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 and JavaScript. These general-purpose languages are used to enhance the dynamic ability for Native functions. Take a popular example to explain: the operator dynamically changes the online js file, and the fluent application dynamically renders after pulling and updating through the network. This is the GPL based Native enhancement.
- DSL based Native enhancements. DSL is a special domain language. It is a language specially designed to solve tasks in certain specific scenarios, such as xml, json, css, and html. By generating a simple DSL language file, the page is dynamically configured through the parsing protocol.
Let's look at the dynamic ecology of Flutter as a whole. At present, there is no mature open source framework in the market. Only major domestic Internet companies have opened source one after another, but they are in a state of urgent need of maintenance. Current mainstream frameworks include:
- Tencent open source MXFlutter
- 58 open source in the same city Flutter Fair
- Alibaba open source Kraken, North Sea
At the same time, I will also introduce two other common methods:
- webview enhancement [embedded in Tencent X5 kernel, model upgrading]
- UI library Templating
Comparison of dynamic structures of major factories
Flutter Fair
Fair is a dynamic framework designed by "58 cities" for fluent. It realizes the automatic conversion of JSON configuration and native Dart source files through the Fair Compiler tool, so as to dynamically update the Widget Tree and State.
Introduction to use
The Fair plug-in on the pub is not officially maintained. We need to go to the GitHub fork source code to write the demo. 58Fair
Prepare a configured JSON file, and then directly call the FairWidget to pass in the file path to display it. It is very simple. The dynamic requirement is nothing more than to put the JSON configuration file online, and then the FairWidget will be pulled down and displayed every time, so as to realize dynamic.
///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})}, ), );
Continuing to follow up the source code, we can see that when the file path we passed in starts with http, it will be pulled through the network
void _reload() { var name = state2key; var path = 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); } }); } ///Then enter the decoder → bundle layer by layer through the parse() method_ Provider, view 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 the parsing methods starting with http. The http library is used 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; }
Look at the dependency. In fact, it is very old. It is obvious that the maintenance is not enough; At the same time, there are restrictions on the version of Flutter. Every time a version of Flutter is released, the official 58Fair may need to make an adaptation.
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
Finally, how to write JSON configuration files? You must bring a set of protocols with you, and you can write them along with the official document Api. Students who are familiar with Flutter should be able to understand the following example code.
{ "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 } } } }
Pros and cons analysis
- Benefits of Fair: simple to use, stable performance;
- The disadvantages are obvious:
- Configuring the UI with JSON means that it does not support logic;
- There are too many widget s in fluent, and Fair can only match limited static UI at present;
- Apart from Dart ecology, the UI is written in JSON;
- The maintenance efforts of the team are very limited. Many plug-ins have not been updated, nor has pub been updated. [but in fact, this is a common problem of all Flutter dynamic open source frameworks ðŸ˜]
MxFlutter
Mxshutter also has limited maintenance efforts. Currently, pub is not the latest version. The GitHub address has also been changed. Please see the latest one github.com/Tencent/mxf...
Mxfluent writes Dart through JavaScript. It also loads online js files, which are converted and displayed by the engine at runtime, so as to achieve dynamic effects. The official started to access TypeScript in version 0.7.0, introduced npm ecology, optimized the cost of js development, and further moved closer to the front-end ecology.
Unfortunately, when comparing the schemes of major manufacturers, it is found that mxshuttle has extremely low cost performance and high learning cost, and abandons js ecology and Dart ecology.
Therefore, I only make a simple analysis of the implementation principle of mxfluent without in-depth research.
Introduction to use
- Initialize engine
String jsBundlePath = await _copyBizBundelZipToMXPath(); if (jsBundlePath != null) { // Start mxshutter and load JS library. MXJSFlutter.runJSApp(jsAppPath: jsBundlePath); }
- Pass in the js script through the MXJSPageWidget, and it can be parsed and displayed. In general, mxshutter is used to show an entire page written using the mxshutter framework
Navigator.push( context, MaterialPageRoute( builder: (context) => MXJSPageWidget( jsWidgetName: "mxflutter-ts-demo", flutterPushParams: { "widgetName": "WidgetExamplesPage" }), ), );
- Let's take a look at the interface definition of mxjflutter. We can see that many protocols are defined, which will inevitably increase the learning cost of js. At the same time, it is highly dependent on the engine of mxfluent. Whether the team has the ability to hold the pit should be carefully considered.
abstract class MXJSFlutter { static MXJSFlutter _instance; static String _localJSAppPath; static String get localJSAppPath => _localJSAppPath; ///Get the external interface class mxjsshutter. ///Most interfaces of mxfluent are called through mxjsfluent. static MXJSFlutter getInstance() { if (_instance == null) { _instance = _MXJSFlutter(); } return _instance; } ///Start JSApp by the Flutter code. You can display the shutter page first, and then Push the route to jump to the JS page. ///After starting the JSApp, execute the JS code. The JS code can actively call the fluent to display its own page, and can also accept the instructions of the fluent to display the corresponding page. /// ///@param jsAppPath jsApp root path, JS business code is placed in a folder. jsAppPath and jsAppAssetsKey are selected according to the scenario. ///@param jsAppAssetsKey use pubspec The AssetsKey configuration in yaml is used to set jsAppPath. By default, it is mxshutter in the same level directory as lib and ios under the shutter project_ js_ Bundle/ folder. ///@param jsExceptionHandler js exception callback. See MXJSExceptionHandler description for method parameters. ///@param debugBizJSPath can only be used under iOS simulator!!! Place the path to the local js directory, and directly place xxx/bundle-xxx js file, no need to package it into bizbundle zip. jsAppPath does not take effect after using this parameter. /// @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 it is necessary to copy main Zip package. MXBundleZipManager bundleManager = MXBundleZipManager(jsAppPath: jsAppPath); MXBundleZipCheckResult result = await bundleManager.checkNeedCopyMainZip(); if (!result.success) { MXJSLog.log( 'MXJSFlutter.runJSApp: checkAppBundleZip failed: ${result.errorMessage}'); // success callback for engine initialization MXJSFlutter.getInstance().jsEngineInitCompletionHandler( {'success': result.success, 'errorMessage': result.errorMessage}); return; } // In the debug state, if debugJSBundlePath is not empty, run the js file in this directory. String realJSAppPath = jsAppPath; if (debugJSBundlePath != null && debugJSBundlePath.isNotEmpty && await canDefineDebugJSBundlePath()) { realJSAppPath = debugJSBundlePath; } _localJSAppPath = realJSAppPath; // Load main js. _callNativeRunJSApp( jsAppPath: realJSAppPath, jsExceptionHandler: jsExceptionHandler); } ///The default hot update directory of JSBundle is used to place the downloaded JS Bundle files static Future<String> defaultJSAppUpdatePath() async { // If the business does not specify a directory, the default directory is used return await Utils.findLocalPath() + Platform.pathSeparator + mxJSAPPDefaultAssetsKey; } ///Allow to define debugJSBundlePath static Future<bool> canDefineDebugJSBundlePath() async { // Currently, only scenarios are supported: 1) iOS simulator of debugging 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 it is main js error. If the arguments['jsFileType'] is 0, the success callback of the js engine will be executed. if (arguments is Map && arguments['jsFileType'] == 0) { MXJSFlutter.getInstance().jsEngineInitCompletionHandler( {'success': false, 'errorMessage': arguments['errorMsg']}); } // Call back to the business side. if (jsExceptionHandler != null) { jsExceptionHandler(arguments); } }); // Initialize MXFFICallbackManager. MXFFICallbackManager.getInstance(); args["flutterAppEnvironmentInfo"] = flutterAppEnvironmentInfo; MXPlatformChannel.getInstance().invokeMethod("callNativeRunJSApp", args); } ///Push a JS page from the shutter ///@param widgetName: "widgetName", in main JS myapp:: used in the createjswidgetwithname function ///MyApp::createJSWidgetWithName create the corresponding JSWidget through the widgetName ///Generally, you should use a higher-level API MXJSPageWidget wrapper class to display JS widgets. Please refer to the usage of MXJSPageWidget dynamic navigatorPushWithName( String widgetName, Key widgetKey, Map flutterPushParams, {String bizPath}); ///Set the processor and customize the Loading widget when the JS page is loaded. void setJSWidgetLoadingHandler(MXWidgetBuildHandler handler); ///Set the processor and customize the Error widget when the JS page is loaded incorrectly. void setJSWidgetBuildErrorHandler(MXWidgetBuildHandler handler); ///JS engine initialization end callback. void jsEngineInitCompletionHandler(dynamic arguments); ///Whether the JS engine has been initialized. bool isJSEngineInit(); ///Setting JS engine initialized. void setJSEngineInit(); ///JS engine initialization results. Map<String, dynamic> jsEngineInitResult(); ///Recreate mxjsshutter, including channel and attribute. void resetup(); ///Current flutterApp. MXJSFlutterApp get currentApp; }
Kraken
Kraken is a high-performance rendering engine based on W3C standard, which is open source by Alibaba. It is also the library with the highest maintenance efforts within the framework of several large factories. See GitHub . Kraken's advantage is that it can develop based on W3C, introduces npm ecology, and supports the development using Vue and React frameworks. Generally, front-end personnel can develop, and the learning cost is very low.
Introduction to use
pubspec, and then directly use Widget Kraken to pass in the url of the script.
kraken: ^0.9.0
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( 'Product details', style: Theme.of(context).textTheme.headline6, ), ), ), body: kraken, ); }
It can be seen that the focus is on how to maintain js files with dynamic operation content, which is Kraken's most competitive point compared with other frameworks, Official api It is written in great detail. Based on the W3C standard, it can be developed using the 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>
The disadvantage of Kraken is that it does not support css style, which makes the Vue development experience relatively general. But in general, it has been very good. The official maintenance is strong, which meets the front-end ecology and is easy to use. It is a good choice for dynamic technology.
Webview enhanced optimization
Almost all mobile applications use webview as a container for h5. Generate h5 through the operation platform configuration, and the app can be displayed directly. Unfortunately, there are many problems with the experience, stability / compatibility of webview.
The loading process of the experience is blank, and the loading process and error status cannot be defined; In terms of compatibility, iOS is good, and the browser kernel is WKWebView. However, Android devices are diverse, and the browser kernel is uneven, so there are often problems in compatibility. To solve the above problems, we are based on the official plug-in webview_flutter has made the following scheme:
- In terms of experience, modify the webview plug-in to configurable transparent background, and remove the loading bar; The Flutter layer develops an enhanced container for webview, which can define the views that are loading and failed to load, and achieve the loading effect that basically conforms to the app
- In terms of stability, we adopt the method of uniformly implanting X5 kernel
Why X5 kernel?
At present, there are few open source browser kernel SDKs, mainly including ChromeView, Crosswalk and TbsX5.
- Open source based on Chromium kernel ChromeView At present, there is basically no maintenance. Another problem is that the compiled dynamic libraries are too large, such as ARM-29M and x86-38M. This is undoubtedly a big problem for the size of the app. Therefore, ChromeView based on Chromium is abandoned.
- Crosswalk is also based on the Chromium kernel. It also has the above app volume problem, so it is also abandoned.
- TbsX5 Based on the Google Blink kernel, the ecosystem is very mature in China. As long as the mobile phones equipped with wechat support X5. X5 provides two integration schemes:
- x5 kernel (for share) that only shares wechat Q space
- Download x5 kernel independently (with download)
Optimize the experience
- Modify webview_flutter is a configurable transparent background. Please refer to my last article for details #How do I add a transparent background to the shutter WebView?
Final business layer code:
WebView( initialUrl: 'https://www.baidu.com', transparentBackground: true )
- Build the webview container. After the webview background processing is transparent, through the Stack layout and listening to onProgress callback, the webview container is given the effect of loading and loading failure, so that the user experience is similar to that of the native application.
///We're using a flutter_bloc for 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(), ), ], )
Look at the bloc layer, 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 {}
Implanting X5 kernel
There are also some webview for x5 wheels on the pub, but they are in disrepair for a long time. There is no continuous maintenance, and even null safely is not supported.
So we continue to expand webview_flutter library, create a new webview_flutter_android_x5 module, introducing X5 SDK, focusing on the official webview_flutter_android related functions and Api for replacement development. At the same time, Api is provided to the business layer, and the caller decides whether to enable the x5 kernel.
Implanting X5 requires a certain Native foundation. The source code will not be explained here. If I have the opportunity, I will directly open source a library to get the transparent background and X5 kernel together.
UI component library Templating
In this way, some pits are embedded through UI design, and the operation end stores them in the interface by matching existing components. Each time, the background service is pulled to determine how to display the UI. This is a very general method. It is not so dynamic. You need to design the possible UI first. But it is the most reliable. However, during development, it is necessary to package the UI library and customize the protocol. At the same time, it is necessary to pay great attention to the degradation processing. If the network difference can not pull the background data, how to display the page should be handled well.
Summary
Looking back at the following figure, there is no doubt that Kraken can be most applied to production in terms of frameworks, but what I want to say is that these frameworks are not mature. To be applied to production, the team must have the ability to participate in open source and pit filling. In addition, by the way, Kant, who is preparing to open source Tencent QQ music, can also pay attention to the transformation based on Kraken, which has been applied to internal production and is worth looking forward to. [image upload failed... (image-6f24b3-1638193015374)]
The webview enhancement and UI component templating are relatively reliable methods, and the general team has the ability to maintain them. In these two ways, the colleagues of the operation platform no longer need to learn new APIs, and the development cost of managing the background configuration dynamic content is much lower.