Flutter 插件編寫必知必會

本文目的

  • 介紹包和插件的概念
  • 介紹 flutter 調用平臺特定代碼的機制:Platform Channels,和相關類的經常使用方法
  • 介紹插件開發流程和示例
  • 介紹優化插件的方法:添加文檔,合理設置版本號,添加單元測試,添加持續集成
  • 介紹發佈插件的流程和常見問題

目錄結構

  • 編寫以前
  • Platform Channels
  • 插件開發
  • 優化插件
  • 發佈插件
  • 總結

編寫以前

包(packages)的概念

packages 將代碼內聚到一個模塊中,能夠用來分享代碼。一個 package 最少要包括:html

  • 一個 pubspec.yaml 文件:它定義了包的不少元數據,好比包名,版本,做者等
  • 一個 lib 文件夾,包含了包中的 public 代碼,一個包裏至少會有一個 <package-name>.dart 文件

packages 根據內容和做用大體分爲2類:java

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

編寫平臺特定代碼能夠寫在一個 App 裏,也能夠寫在 package 裏,也就是本文的主題 plugin 。變成 plugin 的好處是便於分享和複用(經過 pubspec.yml 中添加依賴)。linux

Platform Channels

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

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

Platofrm Channel架構圖 ios

Architectural overview: platform channels

經常使用類和主要方法

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,將一些簡單的數據類型,高效地序列化成二進制和反序列化。序列化和反序列化在收/發數據時自動完成,調用者無需關心。c++

type support

插件開發

建立 package

在命令行輸入如下命令,從 plugin 模板中建立新包git

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爲例,介紹開發流程github

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?) {...}
    }
    複製代碼

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) {...}
    }
    複製代碼

4. 在 example 中調用包裏的 dart API

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

優化插件

插件的意義在於複用和分享,開源的意義在於分享和迭代。插件的開發者都但願本身的插件能變得popular。插件發佈到pub.dartlang後,會根據 Popularity ,Health, Maintenance 進行打分,其中 Maintenance 就會看 README, CHANGELOG, 和 example 是否添加了內容。shell

添加文檔

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]替換爲年份+全部者,多個全部者就寫多行。 ubuntu

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:
 - 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#)

複製代碼

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功能而頭疼,能夠本身動手豐衣足食,順便還能爲開源作一點微薄的貢獻!

參考

相關文章
相關標籤/搜索