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功能而头疼,能够本身动手丰衣足食,顺便还能为开源作一点微薄的贡献!

参考

相关文章
相关标签/搜索