Flutter 外掛編寫必知必會
本文目的
- 介紹包和外掛的概念
- 介紹 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的處理器,它會替換之前設定的handler
void setMockMethodCallHandler (Future handler(MethodCall call)); // 用於mock,功能類似上面的方法
複製程式碼
Android 側
MethodChannel
void invokeMethod(String method, Object arguments) // 同dart
void 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的Context
Activity activity() // 返回外掛註冊所在的Activity
PluginRegistry.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-C
flutter 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/example
flutter 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/exmaple
flutter 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 Flutter import 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:
- linux
sudo: false
addons:
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-droid
before_script:
- git clone https://github.com/flutter/flutter.git -b stable --depth 1
- ./flutter/bin/flutter doctor
script:
- ./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 <[email protected]>
,保留尖括號),主頁,版本等資訊是否補全,程式碼是否存在 warnning(會檢測說 test 裡有多餘的 import,實際不是多餘的,可以不理會)等。
2. 釋出
$ flutter packages pub publish
複製程式碼
如果釋出失敗,可以在上面命令後加-v
,會列出詳細釋出過程,確定失敗在哪個步驟,也可以看看issue上的解決辦法。
常見問題
- Flutter 安裝路徑缺少許可權,導致釋出失敗,參考
sudo flutter packages pub publish -v
複製程式碼
- 如何新增多個 uploader?參考
pub uploader add [email protected]
pub uploader remove [email protected] # 如果只有一個uploader,將無法移除
複製程式碼
- curl www.google.com 能成功,但釋出時,在 google 的 oauth 出現 timeout 參考
去掉官方指引裡面對PUB_HOSTED_URL、FLUTTER_STORAGE_BASE_URL的修改,這些修改會導致上傳pub失敗。
總結
本文介紹了一下外掛編寫必知的概念和編寫的基本流程,並配了個簡單的例子(原始碼)。希望大家以後不再為Flutter缺少native功能而頭疼,可以自己動手豐衣足食,順便還能為開源做一點微薄的貢獻!