Significance of lightweight transformation

The core of the lightweight Flutter rendering engine is to take Flutter as a “renderer”. Its only function is to draw the data from the Native terminal into the corresponding interface. All other interactive operations are connected to the Native terminal for processing through the Channel bridge.

  • Reuse all network request logic of Native side, avoid the problem of multi-end request inconsistency due to the introduction of a second set of network library
  • Reuse Native existing local image resources to reduce the waste of repeated resources
  • Reduce the complexity of mixing stack logic, use EngineGroup to manage mixing stack rendering, and the data content is bridged to the Native side, so as to solve the problem of EngineGroup data isolation

Some people might say, why do you want to make a lightweight modification? Wouldn’t it be good to use it directly in Flutter, just like network request, access DIO and other network libraries in Flutter, which is also not complicated.

It is true that for many projects, the significance of introducing Flutter is to reduce development costs and improve development efficiency. However, when you mix up Flutter on a relatively mature native project, the upfront cost is relatively high if all the capabilities of Flutter need to be re-implemented. Take network requests for example. After all the request data has been wrapped up inside a Flutter, a Flutter needs to implement all the logic of the native network request, such as interceptors, encryption, redirection, and so on. Also, if the network logic changes later, both the native side and the Flutter need to be adjusted.

Therefore, the important reason for the lightweight of Flutter is to “reuse as much of the original logic as possible”, such as image frames, networks and burial points, rather than reimplementing them all in Flutter.

At the same time, the lightweight transformation of Flutter is also the best practice of EngineGroup architecture. Under EngineGroup architecture, data sources need to be placed on the native side to ensure data sharing among multiple engines.

Finally, the Flutter lightweight modification is also the best way to gradually access the mixed Flutter. This method can quickly access the Flutter with a small upfront capital cost to improve the development efficiency, and at the same time, it can be replaced with full Flutter development after a large number of Flutter access at a later stage. The interface layer can be easily replaced.

Practice of lightweight transformation

First, we generated the interface protocol and calling code through Pigeon, and the native side was developed based on the current protocol.

However, we need to solve the problem that Pigeon CLI scripts can only have one protocol file.

In the last few articles, we modified the Pigeon code to create a Pigeon folder in the root directory and write different protocols to different files, such as SchemaBookSearchAPI, SchemaUserAPI, etc.

Then modify the previous run_pigeon. Sh script.

#! /bin/sh cd pigeon for file in `ls`; do filename=${file%.*} flutter pub run pigeon --input pigeon/${file%.*}.dart \ --dart_out lib/${file%.*}_api.dart \ --java_out .. /QDReaderGank.App/src/main/java/com/qidian/QDReader/flutter/${file%.*}Api.java \ --java_package "com.qidian.QDReader.flutter" doneCopy the code

The script is pretty simple, just looping through all the files in the Pigeon directory to execute the original CLI script separately.

This generates different call files for multiple protocols, one for each implementation.

In this scheme, each business scenario will create an XXXFlutterActivity, and different protocol implementations will be created by the Native side under XXXAPI.

However, this scheme has a fatal defect, that is, Flutter was originally introduced to improve efficiency. In this scenario, the development of Flutter still requires the manpower of the original side. Although the workload is not much, can we remove this part of manpower?

Therefore, we need to further modify the lightweight Flutter framework.

So, first of all, we borrowed stuff from Pigeon to generate Channel code, and the reason why We used Pigeon to generate code was because Pigeon used BasicMessageChannel for Channel communication, The efficiency is the highest compared to several different channels. Second, the generated code shields some of the original calling methods of the Channel, making the calling more convenient.

So, for now, we have a generic protocol that contains only three methods: Get request, Post request, and ActionURL invocation.

import 'package:pigeon/pigeon.dart';

@HostApi()
abstract class NativeNetApi {
  @async
  String getNativeNetBridge(String path, Map<String, Object> params);

  @async
  String postNativeNetBridge(String path, Map<String, Object> params);

  void doActionUrlCall(String actionUrl);
}
Copy the code

In Android, we created a general FlutterActivity and implemented the method of network request in the protocol. With the help of the previous sections, we can easily implement the following code.

class SingleFlutterActivity : FlutterActivity() { private val engine: FlutterEngine by lazy { val app = activity.applicationContext as QDApplication val dartEntrypoint = DartExecutor.DartEntrypoint( FlutterInjector.instance().flutterLoader().findAppBundlePath(), intent.getStringExtra("EntryName").toString() ) app.engines.createAndRunEngine(activity, dartEntrypoint) } companion object { @JvmStatic fun start(context: Context, flutterEntryName: String) { context.startActivity(Intent(context, SingleFlutterActivity::class.java).also { it.putExtra("EntryName", flutterEntryName) }) } } private class NetBridgeApiImp(val context: Context, val lifecycleScope: LifecycleCoroutineScope) : NetBridgeApi.NativeNetApi { override fun getNativeNetBridge(path: String? , params: MutableMap<String, Any>? , result: NetBridgeApi.Result<String>?) { path? .let { lifecycleScope.launch { try { val data = XXXRetrofitClient.getCommonApi().getNetBridge(path, params) result? .success(data.toString()) } catch (e: Exception) { e.printStackTrace() } } } } override fun postNativeNetBridge(path: String? , params: MutableMap<String, Any>? , result: NetBridgeApi.Result<String>?) { path? .let { lifecycleScope.launch { try { val data = XXXRetrofitClient.getCommonApi().postNetBridge(path, params) result? .success(data.toString()) } catch (e: Exception) { e.printStackTrace() } } } } override fun doActionUrlCall(actionUrl: String?) { if (context is BaseActivity) { context.openInternalUrl(actionUrl) } } } override fun configureFlutterEngine(flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) NetBridgeApi.NativeNetApi.setup(flutterEngine.dartExecutor,  NetBridgeApiImp(this, lifecycleScope)) } override fun provideFlutterEngine(context: Context): FlutterEngine { return engine } override fun onDestroy() { super.onDestroy() engine.destroy() } }Copy the code

In this way, when the Activity is started, the route name of the corresponding Flutter is passed to the corresponding Flutter page.

SingleFlutterActivity.start(activity, "main");
Copy the code

In the Flutter interface, native methods can be easily called by protocol.

void _loadData() async {
  String result = await NativeNetApi().getNativeNetBridge(
    "/apipath/xxxxx",
    {"itemId": 1111, "pg": 1, "pz": 20},
  );
  setState(() {
    model = BookModel.fromJson(json.decode(result)).data?.items ?? [];
  });
}
Copy the code

In this way, the native side of the Flutter only needs to set up a JSSDK-like environment to meet the requirements of mixed development, instead of repeated development based on different interfaces. On the Flutter side, only the API path and parameters need to be set.

Finally, we need to add the encapsulation of common interfaces on the native side. First, we need to implement common Get and Post requests.

@GET("{path}") suspend fun getNetBridge( @Path(value = "path", encoded = true) path: String, @QueryMap param: @JvmSuppressWildcards Map<String, Any>? , ): JsonObject @FormUrlEncoded @POST("{path}") suspend fun postNetBridge( @Path(value = "path", encoded = true) path: String, @FieldMap mapParam: @JvmSuppressWildcards Map<String, Any>? , ): JsonObjectCopy the code

The native side network is still encapsulated using OKHttp. One thing to note here is that in Kotlin Retrofit is used, if the parameter type is Any, the @JVMSuppresswildcards annotation is used to mark Any as Object.

Through the above operation, we can get through the whole link.

Other capabilities that need to be bridged native can be implemented by adding interfaces, such as burying points, adding exposure and clicking interfaces, and invoking the protocol in Flutter.

Development process under lightweight

To develop new business requirements using Flutter, first create the corresponding route name in Flutter and then configure the corresponding business page in Main. Then normal Flutter business development can be carried out. Bridge the interface protocol where network requests need to be bridged native. Debugging of Flutter can be done by Mock, or by adding a layer of Mock configuration to Flutter. This allows Flutter to be developed independently of native compilation, taking advantage of the efficiency of Flutter development.

Once the interface is live, you can publish an AAR to a native project to participate in debugging.

This completes the transformation of closed loop, using lightweight Flutter framework for business development, cut half of the native labor costs, but also improve the degree of the unity of the UI, convenient and visual walkthroughs, in addition, also have reduced to the corresponding test cost, most functions only need to test on a platform, some other compatibility test, Test it on a branch device.

Performance Benchmark

Large data volume scenario

Mock interface data was used to test. The number of characters was 120,000, which should be a relatively large interface in conventional development. After testing, data could be normally transmitted.

  • Test method: Mock Native requests interface data, replaces it with new data, and displays the data on the interface.

  • Test results: The statistical time of Channel was 10 times, the mean value was about 12ms in Debug package, and about 7ms in Release package, which met the use conditions.

Frequent request scenario

Use common interface data to request 10 times continuously. Currently, most of the interface request scenarios in conventional development are 1 to 3 times, which can meet almost all current usage scenarios.

  • Test method: Loop 10 times, continuously call Native API to obtain interface data, and display the returned data on the interface.

  • Test result: The test passed and the data was normally requested and displayed.

Through the above two test scenarios, it can be concluded that the scheme is feasible.

I would like to recommend my website xuyisheng. Top/focusing on Android-Kotlin-flutter welcome you to visit