内容简介:编写平台特定代码可以写在一个 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
-
host
和client
都可以监听这个 platform channels 来收发消息
Platofrm Channel架构图
常用类和主要方法
Flutter 侧
Future invokeMethod (String method, [dynamic arguments]); // 调用方法 void setMethodCallHandler (Future handler(MethodCall call)); //给当前channel设置一个method call的处理器,它会替换之前设置的handler void setMockMethodCallHandler (Future handler(MethodCall call)); // 用于mock,功能类似上面的方法 复制代码
Android 侧
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) // 成功的回调 复制代码
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,将一些简单的数据类型,高效地序列化成二进制和反序列化。序列化和反序列化在收/发数据时自动完成,调用者无需关心。
插件开发
创建 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… )作为范本。
- 如何高效的写 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 <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功能而头疼,可以自己动手丰衣足食,顺便还能为开源做一点微薄的贡献!
参考
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。