一个Flutter中台组件的开发过程

背景问题

Flutter的优势是综合开发效率的提升,但是组件缺失大大限制了他的优势.

举个栗子:

需求功能开发完成后,需要打点上报和回收数据. 使用原生开发,这些功能组件都是现成的,但是如果我们用Flutter来做 发现要考虑的东西还不少.

  1. 客户端 打点模块的设计. 上报策略,防错,防丢失,加密…
  2. 服务端 日志上报,入库…
  3. 数据端 日志自动化生成报表,日志格式校验,日志量监控…
  4. android端和iOS端的日志差异处理

所以放弃从头来写的想法,直接桥接Android和iOS的日志模块.
同理在Flutter组件开发的过程中,尽量避免重复造轮子的行为.例如网络,崩溃上报,一些工具类能复用的就复用,采用了Flutter技术就多想想现在做的工作是否有必要,是否符合其技术目标(综合开发效率的提升).

那么下面就以Flutter日志组件这个例子来展示如何做一个Flutter中台组件

Flutter层的准备工作

创建插件工程

选择kotlin和swift为开发语言

实现消息通信,Dart作为发起方 原生作为接收方(MethodChannel回调)

我定义两个方法,
一个是获取LogSDK里面存储的客户端参数,比如用户UID,系统版本信息,Appsflyer的第三方ID等. 第二个是打点信息按照产品既定的格式收集起来,转发给原生上报.
定义代码如下:

class Logsdk {
  const MethodChannel _channel = const MethodChannel('logsdk');
  // 获取参数信息
  Future<BaseInfo> get baseInfo async {
    BaseInfo baseInfo = BaseInfo._fromMap(
        await _channel.invokeMapMethod<String, dynamic>('getBaseInfo'));
    return baseInfo;
  }
  
  // 打点
  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,
    });

那么

  • 原生端怎么实现Dart发起的方法?
  • 原生端怎么把结果回调给Dart?

两个问题需要解决.

实现消息通信, Dart作为接收方 原生作为发起方(MethodChannel监听)

原生端怎么主动把消息发送给Dart.

譬如有这样一个场景: 用户反馈App卡顿,运营联系上了用户拿到了他的UID,然后根据UID推送消息到用户手机,而推送模块在原生端,于是原生层发送消息给Dart层,Dart层收集必要信息借助原生能力发送到服务器. 从而实现了一个Log收集的链路. 消息接收:

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;
    });
  }

外部使用方式

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

那么这里的问题是客户端怎么把信息发送给dart呢?

Android实现

打开创建插件工程自动帮我们生成的LogsdkPlugin.kt文件
找到 onMethodCall. 这里有两个参数(@NonNull call: MethodCall, @NonNull result: Result). 参数call里面放的是dart层的方法名称以及方法携带过来的附加信息. 参数result用来把执行结果回调给dart层. 具体实现如下:

    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()
            }
        }
    }

那么怎么把消息发送给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);
    }

还是利用MethodChannel这个对象
channel.invokeMethod("errorCode", code)方法的第一个参数是发送给dart的方法名称,第二个参数可以为任意类型. 如果是对象传递,看函数的注释文档,建议使用Map/Json来实现.

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

例如

// 在Android原生上发送
    JSONObject item = new JSONObject();
                            item.put("connected", false);
                            channel.invokeMethod("connection-updated", item.toString());
                            
// dart接收
    Map<String, dynamic> result = jsonDecode(call.arguments);
          _purchaseController.add(new PurchasedItem.fromJSON(result));

也就是说上面的三个问题的解决办法都是利用方法通道了.具体总结起来就是

  1. 原生端怎么实现Dart发起的方法?

利用onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) 的第一个参数

  1. 原生端怎么把结果回调给Dart?

利用onMethodCall(@NonNull call: MethodCall, @NonNull result: Result)的第二个参数

  1. 原生端怎么主动把消息发送给Dart.

利用MethodChannel.invokeMethod()方法

iOS实现

iOS的实现方法和Android大同小异. xCode上打开自动生成的classSwiftLogsdkPlugin 然后编辑自动生成的方法public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {.
FlutterMethodCall里面放的是dart传过来的方法名称和参数信息. FlutterResult用来做回调,传给dart层

    // 无论成功与否原生一定要执行result()方法  否则flutter端会一直等待,因为flutter是单线程模型导致卡死.
    // 另外这段代码是在Flutter的UI线程执行,如果是耗时操作记得切换线程
    public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
        switch call.method {
        case "getPlatformVersion":
            result("iOS \(UIDevice.current.systemVersion)")
            result(nil)
        case "uu":
            // 上报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) {
                    // 统计时长上报
                    EventLogger.shared.logEventCount(EventCombine(code: event), act1 ?? "",act2 ?? "" , act3 ?? "", act4 ?? "",act5 ?? "",count: countValue)
                } else{
                    // 普通上报
                    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()
        if let config = UULogger.shared.config{
            infos["vendor_id"] = config.vendor_id
            // android系统是由google提供的uid 对应iOS?
            infos["idfa"] = config.idfa
            // appsflyers提供的uid
            infos["afid"] = config.afid
            infos["device"] = "ios";
            // 系统语言
            infos["sys_lang"] = UIDevice.current.accessibilityLanguage;
            // 系统版本
            infos["sdk_version"] = UIDevice.current.systemVersion;
            // 渠道
            infos["channel"] = config.referrer;
            // 是否是新用户
            infos["isNew"] =  SwiftLogsdkPlugin.isNew ? "1":"0"
            // 包名
            infos["pkg"] = Bundle.main.infoDictionary?["CFBundleDisplayName"] as? String ?? ""
        }
        result(infos)
    }
}

那么主动发消息给dart利用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)
    }

组件工程化

dart层和原生之间的主从关系

从业务上来看flutter是android 和 iOS的承载,但是看下代码依赖就会发现原生才是flutter的宿主,同时也是各种组件的宿主. 只是这些组件需要遵守一些协议来支持flutter组件. 而其中的MethodChannel就是一个基础的通信协议.

以Android工程为例.

  1. 打开Android的宿主gradle文件. 看到apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"这个就是把flutter依赖进来.
  2. 打开setting.gradle看到

    这里就是把Flutter的组件引入依赖到宿主.

Android怎么配置依赖

  1. 打开原生开发环境.

明白了原生是Flutter组件的宿主后,就可以按照传统的Android/iOS工程打开项目 开发原生代码.

Android:
AS直接打开,flutter组件文件夹下面的example

这就是一个Android工程,直接在下面开发

当然也可以把项目整体当做Android工程打开,但是这样会导致项目代码和依赖超级多,影响导入和编译的速度,从而影响效率.

所以组件开发推荐使用example工程打开,这样还可以在example中写下组件的文档和使用范例.

  1. 依赖现有模块

在Log上报的代码看到,并没有去实现,而是直接使用了StatisticLib.getInstance().onCountReal, StatisticLib其实就是我现有的上传代码,打开组件的Android文件夹下面的build.gradle文件直接添加现有依赖

  1. 组件发布

打开pubspec.yaml,现在组件的依赖方式是基于本地文件路径.

  logsdk:
    path: ../flutter-plugins/packages/plugin_logsdk

修改为

组件开发的时候使用本地路径,组件集成的时候使用git依赖, 这样对开发和集成方来说都相对友好.

iOS怎么配置依赖

  1. 打开原生开发环境

执行 flutter pub get

在example/ios 执行 pod install

打开 Runner.xcworkspace

  1. 依赖现有模块
    打开logsdk.podspec

添加EventLog依赖

然后在主项目的Podfile中添加

以及各种Pod依赖

  pod 'Flutter', :path => 'Flutter'
  pod 'KeychainAccess'
  pod 'FBSDKCoreKit', '~> 5.6.0'
  pod 'AppsFlyerFramework', '5.2.0'
  pod 'EventLog', '~>1.0'
  pod 'SwiftyUserDefaults'

这样就实现了插件工程对EventLog原生模块的依赖引入.

  1. 组件发布

打开pubspec.yaml,现在组件的依赖方式是基于本地文件路径.

  logsdk:
    path: ../flutter-plugins/packages/plugin_logsdk

修改为

组件开发的时候使用本地路径,组件集成方使用git依赖.

组件调试

  1. Android和Flutter可以使用Attach调试. 不用从新编译

  2. iOS上从新执行,使用Debug调试
    使用po+对象内容 的方式来打印

断点调试不要多端同时进行.

总结

  1. 使用Flutter的目标是去提升客户端综合开发效率
  2. 之前没有接触过iOS开发,但是在使用xcode和swift的时候. 大部分问题,都能在androidstudio和kotlin里面找到对应的解决模式.

https://juejin.im/post/5ee25016f265da76da29ed9e

「点点赞赏,手留余香」

    还没有人赞赏,快来当第一个赞赏的人吧!
0 条回复 A 作者 M 管理员
    所有的伟大,都源于一个勇敢的开始!
欢迎您,新朋友,感谢参与互动!欢迎您 {{author}},您在本站有{{commentsCount}}条评论