一個Flutter中臺元件的開發過程

BestNevermore發表於2020-06-12

背景問題

Flutter的優勢是綜合開發效率的提升,但是元件缺失大大限制了他的優勢.

舉個例子:

需求功能開發完成後,需要打點上報和回收資料. 使用原生開發,這些功能元件都是現成的,但是如果我們用Flutter來做 發現要考慮的東西還不少.

  1. 客戶端 打點模組的設計. 上報策略,防錯,防丟失,加密...
  2. 服務端 日誌上報,入庫...
  3. 資料端 日誌自動化生成報表,日誌格式校驗,日誌量監控...
  4. android端和iOS端的日誌差異處理

所以放棄從頭來寫的想法,直接橋接Android和iOS的日誌模組.
同理在Flutter元件開發的過程中,儘量避免重複造輪子的行為.例如網路,崩潰上報,一些工具類能複用的就複用,採用了Flutter技術就多想想現在做的工作是否有必要,是否符合其技術目標(綜合開發效率的提升).

那麼下面就以Flutter日誌元件這個例子來展示如何做一個Flutter中臺元件

Flutter層的準備工作

建立外掛工程

一個Flutter中臺元件的開發過程

選擇kotlin和swift為開發語言

一個Flutter中臺元件的開發過程

實現訊息通訊,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就是一個基礎的通訊協議.

一個Flutter中臺元件的開發過程
以Android工程為例.

  1. 開啟Android的宿主gradle檔案. 看到apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"這個就是把flutter依賴進來.
  2. 開啟setting.gradle看到
    一個Flutter中臺元件的開發過程
    這裡就是把Flutter的元件引入依賴到宿主.

Android怎麼配置依賴

  1. 開啟原生開發環境.

明白了原生是Flutter元件的宿主後,就可以按照傳統的Android/iOS工程開啟專案 開發原生程式碼.

Android: AS直接開啟,flutter元件資料夾下面的example

一個Flutter中臺元件的開發過程
這就是一個Android工程,直接在下面開發

一個Flutter中臺元件的開發過程
當然也可以把專案整體當做Android工程開啟,但是這樣會導致專案程式碼和依賴超級多,影響匯入和編譯的速度,從而影響效率.

一個Flutter中臺元件的開發過程
所以元件開發推薦使用example工程開啟,這樣還可以在example中寫下元件的文件和使用範例.

  1. 依賴現有模組

在Log上報的程式碼看到,並沒有去實現,而是直接使用了StatisticLib.getInstance().onCountReal, StatisticLib其實就是我現有的上傳程式碼,開啟元件的Android資料夾下面的build.gradle檔案直接新增現有依賴

一個Flutter中臺元件的開發過程

  1. 元件釋出

開啟pubspec.yaml,現在元件的依賴方式是基於本地檔案路徑.

  logsdk:
    path: ../flutter-plugins/packages/plugin_logsdk
複製程式碼

修改為

一個Flutter中臺元件的開發過程
元件開發的時候使用本地路徑,元件整合的時候使用git依賴, 這樣對開發和整合方來說都相對友好.

iOS怎麼配置依賴

  1. 開啟原生開發環境

執行 flutter pub get

在example/ios 執行 pod install

開啟 Runner.xcworkspace

  1. 依賴現有模組 開啟logsdk.podspec

一個Flutter中臺元件的開發過程
新增EventLog依賴

一個Flutter中臺元件的開發過程
然後在主專案的Podfile中新增

一個Flutter中臺元件的開發過程
以及各種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
複製程式碼

修改為

一個Flutter中臺元件的開發過程
元件開發的時候使用本地路徑,元件整合方使用git依賴.

元件除錯

  1. Android和Flutter可以使用Attach除錯. 不用從新編譯

    一個Flutter中臺元件的開發過程

  2. iOS上從新執行,使用Debug除錯 使用po+物件內容 的方式來列印

一個Flutter中臺元件的開發過程

斷點除錯不要多端同時進行.

總結

  1. 使用Flutter的目標是去提升客戶端綜合開發效率
  2. 之前沒有接觸過iOS開發,但是在使用xcode和swift的時候. 大部分問題,都能在androidstudio和kotlin裡面找到對應的解決模式.

相關文章