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

背景問題

Flutter的優點是綜合開發效率的提高,可是組件缺失大大限制了他的優點.android

舉個栗子:ios

需求功能開發完成後,須要打點上報和回收數據. 使用原生開發,這些功能組件都是現成的,可是若是咱們用Flutter來作 發現要考慮的東西還很多.git

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

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

那麼下面就以Flutter日誌組件這個例子來展現如何作一個Flutter中臺組件swift

Flutter層的準備工做

建立插件工程

選擇kotlin和swift爲開發語言api

實現消息通訊,Dart做爲發起方 原生做爲接收方(MethodChannel回調)

我定義兩個方法, 一個是獲取LogSDK裏面存儲的客戶端參數,好比用戶UID,系統版本信息,Appsflyer的第三方ID等. 第二個是打點信息按照產品既定的格式收集起來,轉發給原生上報. 定義代碼以下:xcode

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,
    });
複製代碼

那麼bash

  • 原生端怎麼實現Dart發起的方法?
  • 原生端怎麼把結果回調給Dart?

兩個問題須要解決.服務器

實現消息通訊, Dart做爲接收方 原生做爲發起方(MethodChannel監聽)

原生端怎麼主動把消息發送給Dart.markdown

譬若有這樣一個場景: 用戶反饋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裏面找到對應的解決模式.
相關文章
相關標籤/搜索