本文目的
- 介紹包和外掛的概念
- 介紹 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
host
和client
都可以監聽這個 platform channels 來收發訊息
Platofrm Channel架構圖
常用類和主要方法
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,將一些簡單的資料型別,高效地序列化成二進位制和反序列化。序列化和反序列化在收/發資料時自動完成,呼叫者無需關心。
外掛開發
建立 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?) {...
}
}複製程式碼 - java:
3.新增iOS平臺程式碼(.h+.m/.swift)
- 首先確保包中
example
的 iOS 專案能夠build
通過
cd hello/exmapleflutter build ios --no-codesign複製程式碼
- 開啟Xcode,選擇
File >
, 並選擇
Openhello/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) {...
}
}複製程式碼 - Object-C:
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…)作為範本。
- 如何高效的寫 ChangeLog ?github 上有不少工具能減少寫 changeLog 工作量,推薦一個github-changelog-generator,目前僅對 github 平臺有效,能夠基於 tags, issues, merged pull requests,自動生成changelog 檔案。
3. LICENSE
- 如何選擇License
- 如何給github上的庫新增License:看完之後才發現自己的 License 上沒有寫時間和作者
比如 MIT License,要把[yyyy] [name of copyright owner]
替換為年份+所有者
,多個所有者就寫多行。
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#)複製程式碼
釋出外掛
1. 檢查程式碼
$ flutter packages pub publish --dry-run複製程式碼
會提示你專案作者(格式為authar_name <
,保留尖括號),主頁,版本等資訊是否補全,程式碼是否存在 warnning(會檢測說 test 裡有多餘的 import,實際不是多餘的,可以不理會)等。
your_email@email.com>
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,將無法移除複製程式碼
- curl www.google.com 能成功,但釋出時,在 google 的 oauth 出現 timeout 參考
去掉官方指引裡面對PUB_HOSTED_URL、FLUTTER_STORAGE_BASE_URL的修改,這些修改會導致上傳pub失敗。
總結
本文介紹了一下外掛編寫必知的概念和編寫的基本流程,並配了個簡單的例子(原始碼)。希望大家以後不再為Flutter缺少native功能而頭疼,可以自己動手豐衣足食,順便還能為開源做一點微薄的貢獻!