Flutter 插件编写必知必会

栏目: IOS · 发布时间: 5年前

内容简介:编写平台特定代码可以写在一个 App 里,也可以写在
  • 介绍包和插件的概念
  • 介绍 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架构图

Flutter 插件编写必知必会

常用类和主要方法

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
复制代码
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 侧

- (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,将一些简单的数据类型,高效地序列化成二进制和反序列化。序列化和反序列化在收/发数据时自动完成,调用者无需关心。

Flutter 插件编写必知必会

插件开发

创建 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 平台代码待实现类
    ./android/src/main/java/com/hello/hello/InstallPlugin.java
    ./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 平台代码待实现类
    /ios/Classes/HelloPlugin.m
    /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 是否添加了内容。

添加文档

1.README.md

2.CHANGELOG.md

  • 关于写 ChangeLog 的意义和规则:推荐一个网站keepachangelog,和它的项目的[changelog](( github.com/olivierlaca… )作为范本。
    Flutter 插件编写必知必会
  • 如何高效的写 ChangeLog ?github 上有不少 工具 能减少写 changeLog 工作量,推荐一个 github-changelog-generator ,目前仅对 github 平台有效,能够基于 tags, issues, merged pull requests,自动生成changelog 文件。

3. LICENSE

比如 MIT License,要把 [yyyy] [name of copyright owner] 替换为 年份+所有者 ,多个所有者就写多行。

Flutter 插件编写必知必会

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

复制代码
Flutter 插件编写必知必会

发布插件

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,将无法移除
复制代码
  • curlwww.google.com 能成功,但发布时,在 google 的 oauth 出现 timeout 参考

去掉官方指引里面对PUB_HOSTED_URL、FLUTTER_STORAGE_BASE_URL的修改,这些修改会导致上传pub失败。

总结

本文介绍了一下插件编写必知的概念和编写的基本流程,并配了个简单的例子( 源码 )。希望大家以后不再为Flutter缺少native功能而头疼,可以自己动手丰衣足食,顺便还能为开源做一点微薄的贡献!

参考


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Programming Python

Programming Python

Mark Lutz / O'Reilly Media / 2006-8-30 / USD 59.99

Already the industry standard for Python users, "Programming Python" from O'Reilly just got even better. This third edition has been updated to reflect current best practices and the abundance of chan......一起来看看 《Programming Python》 这本书的介绍吧!

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具