Flutter插件包开发
Flutter插件包开发
创建包
Android Studio: Create New Flutter Project>Flutter Plugin>命名(flutter_xxxx)>next>finish
Intellij IDea: File>New>Project>Flutter>Next>Project type select Plugin>Finish
flutter create --org com.baidu.www --template=plugin -i objc -a java flutter_package_app
说明:
- --org选项指定组织(反向域名),Android和iOS的标识符
- -a指定android语言 java、kotlin(默认)
- -i指定iOS语言 objc、swift(默认)
平台通道(platform channel)
“平台特定”或“特定平台”中的平台指的就是Flutter应用程序运行的平台,如Android或IOS。
一个完整的Flutter应用程序实际上包括原生代码和Flutter代码两部分。由于Flutter本身只是一个UI系统,它本身是无法提供一些系统能力,比如使用蓝牙、相机、GPS等,因此要在Flutter APP中调用这些能力就必须和原生平台进行通信。为此,Flutter中提供了一个平台通道,用于Flutter和原生平台的通信。
Flutter使用了一个灵活的系统,允许开发人员调用特定平台的API(无论在Android上的Java或Kotlin代码中,还是iOS上的ObjectiveC或Swift代码中均可用)。
Flutter与原生之间的通信依赖灵活的消息传递方式:
- 应用的Flutter部分通过平台通道将消息发送到其应用程序的所在的原生宿主(iOS或Android)应用。
- 宿主监听平台通道,并接收该消息。然后它会调用该平台的API,并将响应发送回客户端,即应用程序的Flutter部分。
消息传递是异步的,确保用户界面在消息传递时不会被挂起(卡顿)。
当在Flutter中调用原生方法时,调用信息通过平台通道传递到原生,原生收到调用信息后方可执行指定的操作,如需返回数据,则原生会将数据再通过平台通道传递给Flutter。
在客户端,MethodChannel API 可以发送与方法调用相对应的消息。 在宿主平台上,MethodChannel Android API 和 FlutterMethodChannel iOS API可以接收方法调用并返回结果。这些类可以用很少的代码就能开发平台插件。
方法调用(消息传递)可以是反向的,即宿主作为客户端调用Dart中实现的API。如: quick_actions插件。
除了上面提到的MethodChannel,还可以使用BasicMessageChannel,它支持使用自定义消息编解码器进行基本的异步消息传递。 此外,可以使用专门的BinaryCodec、StringCodec和 JSONMessageCodec类,或创建自己的编解码器。
MethodChannel // Flutter与原生方法相互调用,用于方法掉用
BasicMessageChannel // Flutter与原生相互发送消息,用于数据传递
EventChannel // 原生发送消息,Flutter接收,用于数据流通信
平台通道数据类型支持
平台通道使用标准消息编/解码器对消息进行编解码,它可以高效的对消息进行二进制序列化与反序列化。
当在发送和接收值时,这些值在消息中的序列化和反序列化会自动进行。
如何获取平台信息
1 | Flutter 中提供了一个全局变量defaultTargetPlatform来获取当前应用的平台信息,defaultTargetPlatform定义在"platform.dart"中,它的类型是TargetPlatform,这是一个枚举类,定义如下: |
1 | 由于不同平台有它们各自的交互规范,Flutter Material库中的一些组件都针对相应的平台做了一些适配,比如路由组件MaterialPageRoute,它在android和ios中会应用各自平台规范的切换动画。那如果想让APP在所有平台都表现一致,比如希望在所有平台路由切换动画都按照ios平台一致的左右滑动切换风格该怎么做?Flutter中提供了一种覆盖默认平台的机制,可以通过显式指定debugDefaultTargetPlatformOverride全局变量的值来指定应用平台。比如: |
MethodChannel(互相调用方法)
Android调用Flutter方法
Android
初始化MethodChannel
1 | //初始化,传递1. flutterView(MainActivity中getFlutter获取),2. name常量,Flutter中使用同名常量 |
调用Flutter方法
1 | private void invokeFlutterMethod() { |
通过MethodChannel调用invokeMethod("方法名","传递参数",[Flutter返回参数回调,非必须]);
Flutter
初始化MethodChannel
1 | static const methodChannel = const MethodChannel('testflutter'); |
添加处理方法到MethodChannel
1 | methodChannel.setMethodCallHandler(_addNativeMethod); |
处理android调用的方法,根据方法名
1 | Future<dynamic> _addNativeMethod(MethodCall methodCall) async { |
Flutter调用Android方法
Android
初始化MethodChannel,并添加自定义plugin
1 | MethodChannel methodChannel = new MethodChannel(flutterView, METHOD_CHANNEL); |
自定义的plugin实现MethodChannel.MethodCallHandler接口的onMethodCall方法
1 | public void onMethodCall(MethodCall methodCall, MethodChannel.Result result) { |
Flutter
初始化MethodChannel
1 | static const methodChannel = const MethodChannel('testflutter'); |
调用Android的方法,接收返回数据
1 | //方法通道的方法是异步的 |
BasicMessageChannel(互相发送消息)
Android给Flutter发消息
Android
初始化BasicMethodChannel
1 | BasicMessageChannel messageChannel = new BasicMessageChannel<>(flutterView, "messageChannel", StandardMessageCodec.INSTANCE); |
调用发送消息的方法
1 | private void sendMessageToFlutter() { |
Flutter
初始化BasicMessageChannel
1 | static const basicMessageChannel = BasicMessageChannel('messageChannel', StandardMessageCodec()); |
添加接收信息处理方法
1 | void _listenMessageFromNative() { |
处理接收的数据
1 | //Flutter接收Native发来的消息 |
Flutter给Android发消息
Android:
初始化BasicMessageChannel并添加plugin给handler
1 | BasicMessageChannel messageChannel = new BasicMessageChannel<>(flutterView, "messageChannel", StandardMessageCodec.INSTANCE); |
plugin实现BasicMessageChannel.MessageHandler接口的onMessage方法,处理接收到的信息
1 | public void onMessage(Object o, BasicMessageChannel.Reply reply) { |
Flutter
初始化BasicMessageChannel
1 | static const basicMessageChannel = BasicMessageChannel('messageChannel', StandardMessageCodec()); |
发送消息给Android并接收返回数据
1 | Future<dynamic> _sendMessageToNative(String message) async { |
EventChannel(原生发送消息,Flutter接收)
Android:
初始化EventChannel并添加plugin给handler
1 | EventChannel eventChannel = new EventChannel(flutterView, EVENT_CHANNEL); |
plugin实现EventChannel.StreamHandler接口及onListen、onCancel方法
在onListen中通过EventChannel.EventShink的实例发消息给Flutter
1 | public void onListen(Object o, EventChannel.EventSink eventSink) { |
Flutter
初始化EventChannel
1 | static const _eventChannel = const EventChannel('charging'); |
添加接收数据方法
1 | void listenNativeEvent() { |
桥接View给Flutter使用
Android
自定义View,继承自PlatformView
1 | public class MyTextview implements PlatformView { |
实现PlatformViewFactory
1 | public class TextViewFactory extends PlatformViewFactory { |
注册View给Flutter使用
1 | registrar.platformViewRegistry().registerViewFactory("TextView", new TextViewFactory(new StandardMessageCodec())); |
Flutter
使用桥接的View
1 | AndroidView( |
插件的使用
在yaml文件和其他dependencies一样使用。
Git packages(远端)
代码上传到Git,并打一个tag
yaml文件引用
1 | dependencies: |
本地
在Flutter App根目录下创建plugins文件夹,把插件移动到plugins下。
1 | dependencies: |
以上限于在创建工程的时候,使用的是plugins创建的,有时候会在自己的Android或iOS工程内部开发,就不这么方便分离发布了。
有时候需要到UI thread执行channelMethod,在Android上需要post一个Runnable到Android UI线程。
1 | new Handler(Looper.getMainLooper()).post(new Runnable() { |
- 所谓的“传View”的本质是传递纹理ID,我们只需要明白Flutter是通过Presentation实现了外接纹理,在创建Presentation时,传入FlutterView对应的Context和创建出来的一个虚拟显示屏对象,使得Flutter可以直接通过ID找到并使用Native创建出来的纹理数据。
- 事件处理,从Native传递到Flutter这一阶段Flutter按照自己的规则处理事件,如果AndroidView获取到了事件,事件会被封装成相应的Native端的事件通过方法通道传回Native,Native再处理事件。
对于可能出现的滑动时间冲突,可以参考官方注释:
1 | /// For example, with the following setup vertical drags will not be dispatched to the Android view as the vertical drag gesture is claimed by the parent [GestureDetector]. |
发布pub使用
package发布位置可以选择:
- pub.dev
- 私有pub仓库(集团pub仓库)
- git仓库
在发布package之前,要先检查这几个文件:pubspec.yaml、README.md、CHANGELOG.md 确保这几个文件完整。
- pubspec.yaml配置文件的配置:
- name: xxxx ## pub库名称
- version: 0.0.1 ## 当前插件版本 确保每次发布与之前的版本不一致
- description: xxxxxxxxx ## pub库简介
- homepage:https://github.com/xxxx ## pub库git地址
- publish_to:http://pub.100tal.com/ ## 私有pub库上传地址 如果不是私有pub仓库 不用配置
- author:xxxxxx@100tal.com ## pub库作者
- 检测配置文件:
flutter packages pub publish --dry-run - 发布
flutter packages pub publish
发布过程中可能会遇到的问题记录
1)author 在最新版本进行‘flutter packages pub publish --dry-run’检测的时候 会报警告 需要移除掉才能检测通过。
2)执行发布命令的时候 会遇到需要登录以下地址去授权的情况 需要登录google账号 请使用chrome打开去验证。
- 验证通过-开始上传-上传成功去 pub.dev 或者自己的私有pub仓库 查看是否发布成功 。
- 验证成功-开始上传-如果超时未成功则需要绕过google授权重新上传。
3)跳过google验证方法;
下载pub项目[下载地址:https://github.com/ameryzhu/pub]
使用Android Studio打开下载的项目 并在Terminal顺序执行:
pub get;
dart--snapshot=mypub.dart.snapshot bin/pu b.dart #执行完这个命令会在pub项目根目录下生成一个mypub.dart.snapshot文件 ;
4)把这个文件放到 ${flutterSDK Path}/bin/cache /dart-sdk/bin/snapshots/ 目录下;
5)然后用编辑器打开${flutterSDKPath}/bin/cache /dart-sdk/bin/pub 文件;
6)把文件倒数第三行的:pub.dart.snapshot 替换为 mypub.dart.snapshot ;
7)保存-退出
8)重新执行发布命令
如果执行了上述方法,在项目中执行pub get的时候会有版本冲突的错误,需要将上述方法修改的pub文件中的 mypub.dart.snapshot 恢复改为pub.dart.snaps hot 。
使用 Github Action 发布 Flutter 插件
首先在插件的 .github/workflows 目录内创建一个配置文件
publish.yml
1 | name: Publish to Pub.dev |
流程中需要设置 OAUTH_ACCESS_TOKEN 和 OAUTH_REFRESH_TOKEN 这两个 Token,他们在 .pub-cache/credentials.json 的文件内,这个文件是第一次手动发布插件成功后自动生成的,在用户的 home 目录或者是安装 Flutter SDK 目录内。
拿到 Token 后去插件仓库添加以上两个 Secret,至此配置工作已完成
发布插件
现在每次更新插件只需要新增标签然后推送到仓库,就可以自动更新插件啦!
1 | git tag v1.0.1 |
下面以 install_plugin 为例,介绍开发流程
1.定义包的 API(.dart)
1 | class InstallPlugin { |
2.添加 Android 平台代码(.java/.kt)
- 首先确保包中
example
的 Android 项目能够build
通过
1 | cd hello/example |
在 AndroidStudio 中选择菜单栏
File > New > Import Project…
, 并选择hello/example/android/build.gradle
导入等待 Gradle sync
运行 example app
找到 Android 平台代码待实现类
1
2./android/src/main/java/com/hello/hello/InstallPlugin.java
./android/src/main/kotlin/com/zaihui/hello/InstallPlugin.kt1
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
36class InstallPlugin(private val registrar: Registrar) : MethodCallHandler {
companion object {
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 | cd hello/exmaple |
打开Xcode,选择
File > Open
, 并选择hello/example/ios/Runner.xcworkspace
找到 iOS 平台代码待实现类
1
2/ios/Classes/HelloPlugin.m
/ios/Classes/SwiftInstallPlugin.swift1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24import 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) {...}
}
编写单元测试
plugin的单元测试主要是测试 dart 中代码的逻辑,也可以用来检查函数名称,参数名称与 API定义的是否一致。如果想测试 platform-specify 代码,更多依赖于 example 的用例,或者写平台的测试代码。
因为 InstallPlugin.dart
的逻辑很简单,所以这里只验证验证方法名和参数名。用 setMockMethodCallHandler
mock 并获取 MethodCall,在 test 中用 isMethodCall
验证方法名和参数名是否正确。
1 | void main() { |