Akindone's Studio.

Flutter 插件编写必知必会

字数统计: 2.5k阅读时长: 11 min
2018/12/23 Share

本文目的

  • 介绍包和插件的概念
  • 介绍 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

1
2
3
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

1
2
3
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

1
2
3
void error(String errorCode, String errorMessage, Object errorDetails) // 异常回调方法
void notImplemented() // 未实现的回调
void success(Object result) // 成功的回调

PluginRegistry

1
2
3
4
5
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

1
2
3
4
5
6
7
8
- (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 模板中创建新包

1
2
3
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)

1
2
3
4
5
6
7
8
9
10
11
12
13
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 通过

    1
    2
    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
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      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 通过

    1
    2
    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
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      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 是否添加了内容。

添加文档

1. README.md

  • 分享一个模版
    和我自己写的简陋版README

    2. CHANGELOG.md

  • 关于写 ChangeLog 的意义和规则:推荐一个网站keepachangelog,和它的项目的changelog作为范本。
    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验证方法名和参数名是否正确。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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 中哦,注意替换路径和分支。

1
[![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. 检查代码

1
$ flutter packages pub publish --dry-run

会提示你项目作者(格式为authar_name <your_email@email.com>,保留尖括号),主页,版本等信息是否补全,代码是否存在 warnning(会检测说 test 里有多余的 import,实际不是多余的,可以不理会)等。

2. 发布

1
$ flutter packages pub publish

如果发布失败,可以在上面命令后加-v,会列出详细发布过程,确定失败在哪个步骤,也可以看看issue上的解决办法。

常见问题

  • Flutter 安装路径缺少权限,导致发布失败,参考

    1
    sudo flutter packages pub publish -v
  • 如何添加多个 uploader?参考

    1
    2
    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功能而头疼,可以自己动手丰衣足食,顺便还能为开源做一点微薄的贡献!

参考

CATALOG
  1. 1. 本文目的
  2. 2. 目录结构
  3. 3. 编写之前
    1. 3.1. 包(packages)的概念
  4. 4. Platform Channels
    1. 4.1. 常用类和主要方法
      1. 4.1.1. Flutter 侧
        1. 4.1.1.1. MethodChannel
      2. 4.1.2. Android 侧
        1. 4.1.2.1. MethodChannel
        2. 4.1.2.2. MethodChannel.Result
        3. 4.1.2.3. PluginRegistry
      3. 4.1.3. iOS 侧
        1. 4.1.3.1. FlutterMethodChannel
    2. 4.2. Platform Channel 所支持的类型
  5. 5. 插件开发
    1. 5.1. 创建 package
    2. 5.2. 实现 package
      1. 5.2.1. 1.定义包的 API(.dart)
      2. 5.2.2. 2.添加 Android 平台代码(.java/.kt)
      3. 5.2.3. 3.添加iOS平台代码(.h+.m/.swift)
      4. 5.2.4. 4. 在 example 中调用包里的 dart API
      5. 5.2.5. 5. 运行 example 并测试平台功能
  6. 6. 优化插件
    1. 6.1. 添加文档
      1. 6.1.1. 1. README.md
      2. 6.1.2. 2. CHANGELOG.md
      3. 6.1.3. 3. LICENSE
      4. 6.1.4. 4. 给所有public的API添加 documentation
    2. 6.2. 合理设置版本号
    3. 6.3. 编写单元测试
    4. 6.4. 添加CI
  7. 7. 发布插件
    1. 7.1. 1. 检查代码
    2. 7.2. 2. 发布
      1. 7.2.1. 常见问题
  8. 8. 总结
  9. 9. 参考