Background the problem

Flutter’s advantage is an increase in overall development efficiency, but its lack of components greatly limits its advantage.

Here’s an example:

After the requirement function is developed, it is necessary to report and recover data. With native development, these functional components are already available, but there are many things to consider if we use Flutter for discovery.

  1. Design of client dotting module. Reporting strategy, error prevention, loss prevention, encryption…
  2. Server logs are reported and stored in the database…
  3. Data side log automatic report generation, log format verification, log quantity monitoring…
  4. Handle the difference between Android and iOS logs

So instead of writing from scratch, bridge the Android and iOS logging modules directly. Similarly, during the development of Flutter components, repeat wheel building should be avoided as much as possible. For example, the network, crash reporting, some tools can be reused, reuse, use Flutter technology to consider whether the current work is necessary, whether it meets its technical goals (improvement of overall development efficiency).

The following is an example of how to make a Flutter central component using the Flutter log component

Preparation of the Flutter layer

Create plug-in project

Select Kotlin and Swift as the development languages

Implements message communication with Dart as the initiator and natively as the receiver (MethodChannel callback)

I define two methods. One is to obtain client parameters stored in LogSDK, such as user UID, system version information, third-party ID of Appsflyer, etc. The second is to collect the information according to the product’s established format and forward it to the native report. The definition code is as follows:

class Logsdk {
  const MethodChannel _channel = const MethodChannel('logsdk'); Future<BaseInfo> get BaseInfo async {BaseInfo BaseInfo = baseinfo._frommap (await) _channel.invokeMapMethod<String, dynamic>('getBaseInfo'));
    returnbaseInfo; } act(int event, {String count, String act1, String act2, String act3, String act4, String act5}) { _channel.invokeMethod("act", {
      'event': event,
      'count': count,
      'act1': act1,
      'act2': act2,
      'act3': act3,
      'act4': act4,
      'act5': act5,
    });
Copy the code

then

  • How does a native implement the Dart method?
  • How does the native end call back results to Dart?

Two problems need to be solved.

Implements message communication with Dart as receiver natively as initiator (MethodChannel listening)

How the native end actively sends messages to Dart.

For example, there is such a scenario: the user reports that the App is stuck, the operation contacts the user and gets his UID, then pushes the message to the user’s mobile phone according to the UID, and the push module is at the native end. Then the native layer sends the message to the Dart layer, and the Dart layer collects necessary information and sends it to the server with the help of the native ability. Thus a Log collection link is realized. Message receiving:

class Logsdk {
  static const MethodChannel _channel = const MethodChannel('logsdk');

  static StreamController<int> _msgCodeController;

  static Stream<int> get msgCodeUpdated =>
      _msgCodeController.stream;

  init() {
    if (_msgCodeController == null) {
      _msgCodeController = new StreamController.broadcast();
    }
    _channel.setMethodCallHandler((call) {
      switch (call.method) {
        case "errorCode":
          _msgCodeController.add(call.arguments as int);
          break;
        case "msgCode":
          _msgCodeController.add(call.arguments);
          break;
        case "updateCode":
          _msgCodeController.add(new ConnectionResult.fromJSON(result));
        default:
          throw new ArgumentError('Unknown method ${call.method}');
      }
      return null;
    });
  }

Copy the code

External mode of use

    Logsdk.msgCodeUpdated.listen((event) {
      if(event==1) {// do it}});Copy the code

So the question here is how does the client send the information to Dart?

Android implementation

Open the create Plugin project to automatically find onMethodCall for our generated logsdkplugin.kt file. There are two arguments (@nonnull Call: MethodCall, @nonnull result: result). The call parameter contains the dart layer method name and additional information that the method carries. The result argument is used to call back the execution results to the DART layer. The concrete implementation is as follows:

    override fun onMethodCall(@NonNull call: MethodCall.@NonNull result: Result) {
        when (call.method) {
            "act"- > {val event: Int? = call.argument("event") event? .let {val act1: String? = call.argument("act1")
                    val act2: String? = call.argument("act2")
                    val act3: String? = call.argument("act3")
                    val act4: String? = call.argument("act4")
                    val act5: String? = call.argument("act5")
                    val count: String? = call.argument("count")
                    if (count == null) { StatisticLib.getInstance().onCountReal(event!! .1, act1 ? :"", act2 ? :"", act3 ? :"", act4 ? :"", act5 ? :"")}else{ StatisticLib.getInstance().onCountReal(event!! , count.toLong(), act1 ? :"", act2 ? :"", act3 ? :"", act4 ? :"", act5 ? :"")}}}"getBaseInfo"- > {val build: MutableMap<String, Any> = StatisticUtil.uuParams
                build["idfa"] = AppsCache.get().sp().getString(AppSpConstants.GAID, "")
                build["afid"] = AppsFlyerLib.getInstance().getAppsFlyerUID(GlobalLib.getContext())
                build["isNew"] = StatisticUtil.isNew().toString()
                build["pkg"] = GlobalLib.getContext().packageName
                build["device"] = "android"
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                    build["sys_lang"] = Locale.getDefault().toLanguageTag()
                } else {
                    build["sys_lang"] = ""
                }
                build["sdk_version"] = Build.VERSION.SDK_INT.toString()
                build["channel"] = ""

                result.success(build)
            }
            else -> {
                result.notImplemented()
            }
        }
    }
Copy the code

So how do you send a message to DART?

    lateinit var channel: MethodChannel

    override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
        channel = MethodChannel(flutterPluginBinding.getFlutterEngine().getDartExecutor(), "logsdk")
        channel.setMethodCallHandler(this);
    }
    fun sendMsg(code: Int) {
        channel.invokeMethod("errorCode", code);
    }
Copy the code

The first argument to the channel.invokemethod (“errorCode”, code) method is the name of the method sent to dart, and the second argument can be of any type. For object passing, see the function’s comment documentation. Map/Json is recommended.

  /** * Arguments forthe call.
   *
   * <p>Consider using {@link #arguments()} for cases where aparticular run-time type is expected. * Consider using {@link #argument(String)} when that run-time type is {@link Map} or {@link * JSONObject}. */
Copy the code

For example,

// Send on Android native
    JSONObject item = new JSONObject();
                            item.put("connected".false);
                            channel.invokeMethod("connection-updated", item.toString());
                            
/ / the dart to receive
    Map<String, dynamic> result = jsonDecode(call.arguments);
          _purchaseController.add(new PurchasedItem.fromJSON(result));
Copy the code

That is to say, the solution of the above three problems is to use the method channel. That’s what it’s all about

  1. How does a native implement the Dart method?

Uses the first argument to onMethodCall(@nonNULL Call: MethodCall, @nonNULL Result: result)

  1. How does the native end call back results to Dart?

Uses the second argument to onMethodCall(@nonNULL Call: MethodCall, @nonNULL Result: result)

  1. How the native end actively sends messages to Dart.

Using MethodChannel. InvokeMethod () method

The iOS implementation

Open the automatically generated classSwiftLogsdkPlugin on xCode and edit the automatically generated method public func Handle (_ call: FlutterMethodCall, result: @escaping FlutterResult) {. FlutterMethodCall contains method names and parameter information that dart passes. FlutterResult is used to do the callback, which is passed to the DART layer

    // The result() method must be executed regardless of success or failure otherwise the flutter end will wait because the flutter is a single-threaded model that causes the flutter to stall.
    This code is executed on the UI thread of the Flutter. If the Flutter operation is time-consuming, remember to switch the thread
    public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
        switch call.method {
        case "getPlatformVersion":
            result("iOS \(UIDevice.current.systemVersion)")
            result(nil)
        case "uu":
            / / reporting UU
            handleReportUU(call, result)
        case "act":
            handleLogAct(call, result)
        case "report":
            handleReportUU(call, result)
        case "getBaseInfo":
            handleGetBaseInfo(call, result)
        default:
            result(FlutterMethodNotImplemented)}}private func handleLogAct(_ call:FlutterMethodCall,_ result: FlutterResult){
        let arguments = call.arguments as? NSDictionary
        if let _args = arguments{
            let event = _args["event"] as! Int
            let act1 = _args["act1"] as? String
            let act2 = _args["act2"] as? String
            let act3 = _args["act3"] as? String
            let act4 = _args["act4"] as? String
            let act5 = _args["act5"] as? String
            let count = _args["count"] as? String
            if let count = count {
                if let countValue = Int64(count) {
                    // Report the statistics duration
                    EventLogger.shared.logEventCount(EventCombine(code: event), act1 ?? "",act2 ?? "" , act3 ?? "", act4 ?? "",act5 ?? "".count: countValue)
                } else{
                    // This is a normal report
                    EventLogger.shared.logEventCount(EventCombine(code: event), act1 ?? "",act2 ?? "" , act3 ?? "", act4 ?? "",act5 ?? "")
                }
            }
            result(nil)}}private func handleGetBaseInfo(_ call:FlutterMethodCall,_ result: FlutterResult){
        var infos:Dictionary<String.String> = Dictionary(a)if let config = UULogger.shared.config{
            infos["vendor_id"] = config.vendor_id
            // Android system is provided by Google uid corresponding to iOS?
            infos["idfa"] = config.idfa
            // The uid provided by Appsflyers
            infos["afid"] = config.afid
            infos["device"] = "ios";
            // System language
            infos["sys_lang"] = UIDevice.current.accessibilityLanguage;
            // System version
            infos["sdk_version"] = UIDevice.current.systemVersion;
            / / channel
            infos["channel"] = config.referrer;
            // Whether the user is new
            infos["isNew"] =  SwiftLogsdkPlugin.isNew ? "1":"0"
            / / package name
            infos["pkg"] = Bundle.main.infoDictionary?["CFBundleDisplayName"] as? String ?? ""
        }
        result(infos)
    }
}
Copy the code

Then actively send a message to DART using the FlutterMethodChannel.

    public static var channel:FlutterMethodChannel? = nil
    
    public static func register(with registrar: FlutterPluginRegistrar) {
        channel = FlutterMethodChannel(name: "logsdk", binaryMessenger: registrar.messenger())
        let instance = SwiftLogsdkPlugin()
        registrar.addMethodCallDelegate(instance, channel: channel!)
    }
    
    public static func sendMsg(_ errorCode:Int){ channel? .invokeMethod("errorCode", arguments: errorCode)
    }
Copy the code

Component engineering

Master-slave relationship between dart layer and native

Commercially, Flutter is hosted by Android and iOS, but a look at the code dependencies shows that native flutter is the host of flutter, as well as the various components. It is just that these components need to follow some protocol to support flutter components. MethodChannel is a basic communication protocol.

  1. Open the Android host Gradle file. seeapply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"This is the dependency of flutter in.
  2. Open setting.gradle to see

    This is where the components of Flutter are introduced into the dependencies of the host.

How does Android configure dependencies

  1. Open the native development environment.

Knowing that native is the host of the Flutter component, you can open the project to develop native code in a traditional Android/iOS project.

Android: AS opens directly, the example below the flutter component folder

  1. Relying on existing modules

Statisticlib.getinstance ().oncountreal, statisticlib.getInstance (). StatisticLib is basically my existing upload code. Open the build.gradle file in the Android folder of the component and add the existing dependencies directly

  1. Component release

Open pubspec.yaml and now the component depends on the local file path.

  logsdk:
    path: ../flutter-plugins/packages/plugin_logsdk
Copy the code

Modified to

How to configure dependencies in iOS

  1. Open the native development environment

Execute the flutter pub get

Run pod Install in example/ios

Open the Runner. Xcworkspace

  1. Open dependent on existing moduleslogsdk.podspec

Pod 'Flutter', :path => 'Flutter' KeychainAccess' pod 'FBSDKCoreKit', '~> 5.6.0' pod 'AppsFlyerFramework', '5.2.0' pod 'SwiftyUserDefaults', '~>1.0' pod 'Copy the code

This enables the plug-in project to introduce dependencies on the EventLog native module.

  1. Component release

Open pubspec.yaml and now the component depends on the local file path.

  logsdk:
    path: ../flutter-plugins/packages/plugin_logsdk
Copy the code

Modified to

Component debugging

  1. Android and the Flutter can be debugged using Attach. You don’t have to recompile

  2. Run it again on iOS, using Debug to print the Po + object content

Breakpoint debugging should not be done simultaneously.

conclusion

  1. The goal of Flutter is to improve the overall client development efficiency
  2. I had no prior experience with iOS development, but I worked with Xcode and Swift. Most of these problems can be solved in androidstudio and kotlin.