背景問題
Flutter的優勢是綜合開發效率的提升,但是元件缺失大大限制了他的優勢.
舉個例子:
需求功能開發完成後,需要打點上報和回收資料. 使用原生開發,這些功能元件都是現成的,但是如果我們用Flutter來做 發現要考慮的東西還不少.
- 客戶端 打點模組的設計. 上報策略,防錯,防丟失,加密...
- 服務端 日誌上報,入庫...
- 資料端 日誌自動化生成報表,日誌格式校驗,日誌量監控...
- 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));
複製程式碼
也就是說上面的三個問題的解決辦法都是利用方法通道了.具體總結起來就是
- 原生端怎麼實現Dart發起的方法?
利用
onMethodCall(@NonNull call: MethodCall, @NonNull result: Result)
的第一個引數
- 原生端怎麼把結果回撥給Dart?
利用
onMethodCall(@NonNull call: MethodCall, @NonNull result: Result)
的第二個引數
- 原生端怎麼主動把訊息傳送給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的宿主gradle檔案. 看到
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
這個就是把flutter依賴進來. - 開啟setting.gradle看到 這裡就是把Flutter的元件引入依賴到宿主.
Android怎麼配置依賴
- 開啟原生開發環境.
明白了原生是Flutter元件的宿主後,就可以按照傳統的Android/iOS工程開啟專案 開發原生程式碼.
Android: AS直接開啟,flutter元件資料夾下面的example
這就是一個Android工程,直接在下面開發 當然也可以把專案整體當做Android工程開啟,但是這樣會導致專案程式碼和依賴超級多,影響匯入和編譯的速度,從而影響效率. 所以元件開發推薦使用example工程開啟,這樣還可以在example中寫下元件的文件和使用範例.- 依賴現有模組
在Log上報的程式碼看到,並沒有去實現,而是直接使用了StatisticLib.getInstance().onCountReal
, StatisticLib其實就是我現有的上傳程式碼,開啟元件的Android資料夾下面的build.gradle檔案直接新增現有依賴
- 元件釋出
開啟pubspec.yaml,現在元件的依賴方式是基於本地檔案路徑.
logsdk:
path: ../flutter-plugins/packages/plugin_logsdk
複製程式碼
修改為
元件開發的時候使用本地路徑,元件整合的時候使用git依賴, 這樣對開發和整合方來說都相對友好.iOS怎麼配置依賴
- 開啟原生開發環境
執行 flutter pub get
在example/ios 執行 pod install
開啟 Runner.xcworkspace
- 依賴現有模組
開啟
logsdk.podspec
pod 'Flutter', :path => 'Flutter'
pod 'KeychainAccess'
pod 'FBSDKCoreKit', '~> 5.6.0'
pod 'AppsFlyerFramework', '5.2.0'
pod 'EventLog', '~>1.0'
pod 'SwiftyUserDefaults'
複製程式碼
這樣就實現了外掛工程對EventLog原生模組的依賴引入.
- 元件釋出
開啟pubspec.yaml,現在元件的依賴方式是基於本地檔案路徑.
logsdk:
path: ../flutter-plugins/packages/plugin_logsdk
複製程式碼
修改為
元件開發的時候使用本地路徑,元件整合方使用git依賴.元件除錯
-
Android和Flutter可以使用Attach除錯. 不用從新編譯
-
iOS上從新執行,使用Debug除錯 使用po+物件內容 的方式來列印
斷點除錯不要多端同時進行.
總結
- 使用Flutter的目標是去提升客戶端綜合開發效率
- 之前沒有接觸過iOS開發,但是在使用xcode和swift的時候. 大部分問題,都能在androidstudio和kotlin裡面找到對應的解決模式.