Flutter 外掛編寫必知必會

升級之路發表於2018-12-26

本文目的

  • 介紹包和外掛的概念
  • 介紹 flutter 呼叫平臺特定程式碼的機制:Platform Channels,和相關類的常用方法
  • 介紹外掛開發流程和示例
  • 介紹優化外掛的方法:新增文件,合理設定版本號,新增單元測試,新增持續整合
  • 介紹釋出外掛的流程和常見問題

目錄結構

  • 編寫之前
  • Platform Channels
  • 外掛開發
  • 優化外掛
  • 釋出外掛
  • 總結

編寫之前

包(packages)的概念

packages 將程式碼內聚到一個模組中,可以用來分享程式碼。一個 package 最少要包括:

  • 一個 pubspec.yaml 檔案:它定義了包的很多後設資料,比如包名,版本,作者等
  • 一個 lib 資料夾,包含了包中的 public 程式碼,一個包裡至少會有一個 <
    package-name>
    .dart
    檔案

packages 根據內容和作用大致分為2類:

  • Dart packages :程式碼都是用 Dart 寫的
  • Plugin packages :一種特殊的 Dart package,它包括 Dart 編寫的 API ,加上平臺特定程式碼,如 Android (用Java/Kotlin), iOS (用ObjC/Swift)

編寫平臺特定程式碼可以寫在一個 App 裡,也可以寫在 package 裡,也就是本文的主題 plugin 。變成 plugin 的好處是便於分享和複用(通過 pubspec.yml 中新增依賴)。

Platform Channels

Flutter提供了一套靈活的訊息傳遞機制來實現 Dart 和 platform-specific code 之間的通訊。這個通訊機制叫做 Platform Channels

  • Native Platform 是 host ,Flutter 部分是 client
  • hostclient 都可以監聽這個 platform channels 來收發訊息

Platofrm Channel架構圖

Architectural overview: platform channels

常用類和主要方法

Flutter 側

MethodChannel

Future invokeMethod (String method, [dynamic arguments]);
// 呼叫方法void setMethodCallHandler (Future handler(MethodCall call));
//給當前channel設定一個method call的處理器,它會替換之前設定的handlervoid setMockMethodCallHandler (Future handler(MethodCall call));
// 用於mock,功能類似上面的方法複製程式碼

Android 側

MethodChannel

void invokeMethod(String method, Object arguments) // 同dartvoid invokeMethod(String method, Object arguments, MethodChannel.Result callback) // callback用來處理Flutter側的結果,可以為null,void setMethodCallHandler(MethodChannel.MethodCallHandler handler) // 同dart複製程式碼

MethodChannel.Result

void error(String errorCode, String errorMessage, Object errorDetails) // 異常回撥方法void notImplemented() // 未實現的回撥void success(Object result) // 成功的回撥複製程式碼

PluginRegistry

Context context() // 獲取Application的ContextActivity activity() // 返回外掛註冊所在的ActivityPluginRegistry.Registrar addActivityResultListener(PluginRegistry.ActivityResultListener listener) // 新增Activityresult監聽PluginRegistry.Registrar addRequestPermissionsResultListener(PluginRegistry.RequestPermissionsResultListener listener) // 新增RequestPermissionResult監聽BinaryMessenger messenger() // 返回一個BinaryMessenger,用於外掛與Dart側通訊複製程式碼

iOS 側

FlutterMethodChannel

- (void)invokeMethod:(nonnull NSString *)method arguments:(id _Nullable)arguments;
// result:一個回撥,如果Dart側失敗,則回撥引數為FlutterError型別;// 如果Dart側沒有實現此方法,則回撥引數為FlutterMethodNotImplemented型別;// 如果回撥引數為nil獲取其它型別,表示Dart執行成功- (void)invokeMethod:(nonnull NSString *)method arguments:(id _Nullable)arguments result:(FlutterResult _Nullable)callback;
- (void)setMethodCallHandler:(FlutterMethodCallHandler _Nullable)handler;
複製程式碼

Platform Channel 所支援的型別

標準的 Platform Channels 使用StandardMessageCodec,將一些簡單的資料型別,高效地序列化成二進位制和反序列化。序列化和反序列化在收/發資料時自動完成,呼叫者無需關心。

type support

外掛開發

建立 package

在命令列輸入以下命令,從 plugin 模板中建立新包

flutter create --org com.example --template=plugin hello # 預設Android用Java,iOS用Object-Cflutter create --org com.example --template=plugin -i swift -a kotlin hello # 指定Android用Kotlin,iOS用Swift複製程式碼

實現 package

下面以install_plugin為例,介紹開發流程

1.定義包的 API(.dart)

class InstallPlugin { 
static const MethodChannel _channel = const MethodChannel('install_plugin');
static Future<
String>
installApk(String filePath, String appId) async {
Map<
String, String>
params = {'filePath': filePath, 'appId': appId
};
return await _channel.invokeMethod('installApk', params);

} static Future<
String>
gotoAppStore(String urlString) async {
Map<
String, String>
params = {'urlString': urlString
};
return await _channel.invokeMethod('gotoAppStore', params);

}
}複製程式碼

2.新增 Android 平臺程式碼(.java/.kt)

  • 首先確保包中 example 的 Android 專案能夠 build 通過
cd hello/exampleflutter build apk複製程式碼
  • 在 AndroidStudio 中選擇選單欄 File >
    New >
    Import Project…
    , 並選擇 hello/example/android/build.gradle 匯入
  • 等待 Gradle sync
  • 執行 example app
  • 找到 Android 平臺程式碼待實現類
    • java:./android/src/main/java/com/hello/hello/InstallPlugin.java
    • kotlin:./android/src/main/kotlin/com/zaihui/hello/InstallPlugin.kt
    class InstallPlugin(private val registrar: Registrar) : MethodCallHandler { 
    companion object {
    @JvmStatic fun registerWith(registrar: Registrar): Unit {
    val channel = MethodChannel(registrar.messenger(), "install_plugin") val installPlugin = InstallPlugin(registrar) channel.setMethodCallHandler(installPlugin) // registrar 裡定義了addActivityResultListener,能獲取到Acitvity結束後的返回值 registrar.addActivityResultListener {
    requestCode, resultCode, intent ->
    ...
    }
    }
    } override fun onMethodCall(call: MethodCall, result: Result) {
    when (call.method) {
    "installApk" ->
    {
    // 獲取引數 val filePath = call.argument<
    String>
    ("filePath") val appId = call.argument<
    String>
    ("appId") try {
    installApk(filePath, appId) result.success("Success")
    } catch (e: Throwable) {
    result.error(e.javaClass.simpleName, e.message, null)
    }
    } else ->
    result.notImplemented()
    }
    } private fun installApk(filePath: String?, appId: String?) {...
    }
    }複製程式碼

3.新增iOS平臺程式碼(.h+.m/.swift)

  • 首先確保包中 example 的 iOS 專案能夠 build 通過
cd hello/exmapleflutter build ios --no-codesign複製程式碼
  • 開啟Xcode,選擇 File >
    Open
    , 並選擇 hello/example/ios/Runner.xcworkspace
  • 找到 iOS 平臺程式碼待實現類
    • Object-C:/ios/Classes/HelloPlugin.m
    • Swift:/ios/Classes/SwiftInstallPlugin.swift
    import Flutterimport UIKit    public class SwiftInstallPlugin: NSObject, FlutterPlugin { 
    public static func register(with registrar: FlutterPluginRegistrar) {
    let channel = FlutterMethodChannel(name: "install_plugin", binaryMessenger: registrar.messenger()) let instance = SwiftInstallPlugin() registrar.addMethodCallDelegate(instance, channel: channel)
    } public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
    switch call.method {
    case "gotoAppStore": guard let urlString = (call.arguments as? Dictionary<
    String, Any>
    )?["urlString"] as? String else {
    result(FlutterError(code: "引數異常", message: "引數url不能為空", details: nil)) return
    } gotoAppStore(urlString: urlString) default: result(FlutterMethodNotImplemented)
    }
    } func gotoAppStore(urlString: String) {...
    }
    }複製程式碼

4. 在 example 中呼叫包裡的 dart API

5. 執行 example 並測試平臺功能

優化外掛

外掛的意義在於複用和分享,開源的意義在於分享和迭代。外掛的開發者都希望自己的外掛能變得popular。外掛釋出到pub.dartlang後,會根據 Popularity ,Health, Maintenance 進行打分,其中 Maintenance 就會看 README, CHANGELOG, 和 example 是否新增了內容。

新增文件

1. README.md

2. CHANGELOG.md

  • 關於寫 ChangeLog 的意義和規則:推薦一個網站keepachangelog,和它的專案的[changelog]((github.com/olivierlaca…)作為範本。
    keepachangelog principle and types
  • 如何高效的寫 ChangeLog ?github 上有不少工具能減少寫 changeLog 工作量,推薦一個github-changelog-generator,目前僅對 github 平臺有效,能夠基於 tags, issues, merged pull requests,自動生成changelog 檔案。

3. LICENSE

比如 MIT License,要把[yyyy] [name of copyright owner]替換為年份+所有者,多個所有者就寫多行。

license-ownner-year

4. 給所有public的API新增 documentation

合理設定版本號

在姊妹篇Flutter 外掛使用必知必會中已經提到了語義化版本的概念,作為外掛開發者也要遵守

版本格式:主版本號.次版本號.修訂號,版本號遞增規則如下:

  • 主版本號:當你做了不相容的 API 修改,
  • 次版本號:當你做了向下相容的功能性新增,
  • 修訂號:當你做了向下相容的問題修正。

編寫單元測試

plugin的單元測試主要是測試 dart 中程式碼的邏輯,也可以用來檢查函式名稱,引數名稱與 API定義的是否一致。如果想測試 platform-specify 程式碼,更多依賴於 example 的用例,或者寫平臺的測試程式碼。

因為InstallPlugin.dart的邏輯很簡單,所以這裡只驗證驗證方法名和引數名。用setMockMethodCallHandler mock 並獲取 MethodCall,在 test 中用isMethodCall驗證方法名和引數名是否正確。

void main() { 
const MethodChannel channel = MethodChannel('install_plugin');
final List<
MethodCall>
log = <
MethodCall>
[];
String response;
// 返回值 // 設定mock的方法處理器 channel.setMockMethodCallHandler((MethodCall methodCall) async {
log.add(methodCall);
return response;
// mock返回值
});
tearDown(() {
log.clear();

});
test('installApk test', () async {
response = 'Success';
final fakePath = 'fake.apk';
final fakeAppId = 'com.example.install';
final String result = await InstallPlugin.installApk(fakePath, fakeAppId);
expect( log, <
Matcher>
[isMethodCall('installApk', arguments: {'filePath': fakePath, 'appId': fakeAppId
})], );
expect(result, response);

});

}複製程式碼

新增CI

持續整合(Continuous integration,縮寫CI),通過自動化和指令碼來驗證新的變動是否會產生不利影響,比如導致建構失敗,單元測試break,因此能幫助開發者儘早發現問題,減少維護成本。對於開源社群來說 CI 尤為重要,因為開源專案一般不會有直接收入,來自 contributor 的程式碼質量也良莠不齊。

我這裡用 Travis 來做CI,入門請看這裡travis get stated

在專案根目錄新增 .travis.yml 檔案

os:  - linuxsudo: falseaddons:  apt:    sources:      - ubuntu-toolchain-r-test # if we don't specify this, the libstdc++6 we get is the wrong version    packages:      - libstdc++6      - fonts-droidbefore_script:  - git clone https://github.com/flutter/flutter.git -b stable --depth 1  - ./flutter/bin/flutter doctorscript:  - ./flutter/bin/flutter test # 跑專案根目錄下的test資料夾中的測試程式碼cache:  directories:    - $HOME/.pub-cache複製程式碼

這樣當你要提 PR 或者對分支做了改動,就會觸發 travis 中的任務。還可以把 build 的小綠標新增到 README.md 中哦,注意替換路徑和分支。

[![Build Status](https://travis-ci.org/hui-z/flutter_install_plugin.svg?branch=master)](https://travis-ci.org/hui-z/flutter_install_plugin#)複製程式碼
travis ci

釋出外掛

1. 檢查程式碼

$ flutter packages pub publish --dry-run複製程式碼

會提示你專案作者(格式為authar_name <
your_email@email.com>
,保留尖括號),主頁,版本等資訊是否補全,程式碼是否存在 warnning(會檢測說 test 裡有多餘的 import,實際不是多餘的,可以不理會)等。

2. 釋出

$ flutter packages pub publish複製程式碼

如果釋出失敗,可以在上面命令後加-v,會列出詳細釋出過程,確定失敗在哪個步驟,也可以看看issue上的解決辦法。

常見問題

  • Flutter 安裝路徑缺少許可權,導致釋出失敗,參考
sudo flutter packages pub publish -v複製程式碼
  • 如何新增多個 uploader?參考
 pub uploader add bob@example.com pub uploader remove bob@example.com # 如果只有一個uploader,將無法移除複製程式碼

去掉官方指引裡面對PUB_HOSTED_URL、FLUTTER_STORAGE_BASE_URL的修改,這些修改會導致上傳pub失敗。

總結

本文介紹了一下外掛編寫必知的概念和編寫的基本流程,並配了個簡單的例子(原始碼)。希望大家以後不再為Flutter缺少native功能而頭疼,可以自己動手豐衣足食,順便還能為開源做一點微薄的貢獻!

參考

來源:https://juejin.im/post/5c22e9eff265da61715e5f46

相關文章