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

说明:

  1. --org选项指定组织(反向域名),Android和iOS的标识符
  2. -a指定android语言 java、kotlin(默认)
  3. -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
2
3
4
5
6
7
8
Flutter 中提供了一个全局变量defaultTargetPlatform来获取当前应用的平台信息,defaultTargetPlatform定义在"platform.dart"中,它的类型是TargetPlatform,这是一个枚举类,定义如下:
enum TargetPlatform {
android,
fuchsia,
iOS,
}

判断平台:if(defaultTargetPlatform==TargetPlatform.android){}
1
2
3
4
由于不同平台有它们各自的交互规范,Flutter Material库中的一些组件都针对相应的平台做了一些适配,比如路由组件MaterialPageRoute,它在android和ios中会应用各自平台规范的切换动画。那如果想让APP在所有平台都表现一致,比如希望在所有平台路由切换动画都按照ios平台一致的左右滑动切换风格该怎么做?Flutter中提供了一种覆盖默认平台的机制,可以通过显式指定debugDefaultTargetPlatformOverride全局变量的值来指定应用平台。比如:
debugDefaultTargetPlatformOverride=TargetPlatform.iOS;
print(defaultTargetPlatform); // 会输出TargetPlatform.iOS
上面代码即在Android中运行后,Flutter APP就会认为是当前系统是iOS,Material组件库中所有组件交互方式都会和iOS平台对齐,defaultTargetPlatform的值也会变为TargetPlatform.iOS。

MethodChannel(互相调用方法)

Android调用Flutter方法

Android

初始化MethodChannel

1
2
//初始化,传递1. flutterView(MainActivity中getFlutter获取),2. name常量,Flutter中使用同名常量
MethodChannel methodChannel = new MethodChannel(flutterView, “testflutter”);

调用Flutter方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private void invokeFlutterMethod() {
if (this.mMethodChannel != null) {
this.mMethodChannel.invokeMethod("flutterMethod", "native参数", new MethodChannel.Result() {
@Override
public void success(Object o) {
Toast.makeText(mContext, o.toString(), Toast.LENGTH_LONG).show();
}
@Override
public void error(String s, String s1, Object o) {
}
@Override
public void notImplemented() {
}
});
}
}

通过MethodChannel调用invokeMethod("方法名","传递参数",[Flutter返回参数回调,非必须]);

Flutter

初始化MethodChannel

1
static const methodChannel = const MethodChannel('testflutter');

添加处理方法到MethodChannel

1
methodChannel.setMethodCallHandler(_addNativeMethod);

处理android调用的方法,根据方法名

1
2
3
4
5
6
7
8
9
10
11
Future<dynamic> _addNativeMethod(MethodCall methodCall) async {
switch (methodCall.method) {
case 'flutterMethod':
setState(() {
_calledFromNative = 'flutter method called from native with param ' + methodCall.arguments;
});
return 'flutter method called from native with param ' + methodCall.arguments;
break;
}
}
//其中,return返回的数据在Android的回调中接收

Flutter调用Android方法

Android

初始化MethodChannel,并添加自定义plugin

1
2
MethodChannel methodChannel = new MethodChannel(flutterView, METHOD_CHANNEL);
methodChannel.setMethodCallHandler(plugin);

自定义的plugin实现MethodChannel.MethodCallHandler接口的onMethodCall方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void onMethodCall(MethodCall methodCall, MethodChannel.Result result) {
// Flutter调用Native的方法
if (methodCall.method.equals("getBatteryLevel")) {
int batteryLevel = getBatteryLevel();
if (batteryLevel != -1) {
result.success(batteryLevel);
} else {
result.error("UNAVALIABLE", "battery level unavaliable", null);
}
} else {
result.notImplemented();
}
}
//在onMethodCall中监听Flutter调用什么名字的方法(此处getBatterLevel),通过result返回方法的执行结果。
Flutter

初始化MethodChannel

1
static const methodChannel = const MethodChannel('testflutter');

调用Android的方法,接收返回数据

1
2
3
4
5
6
7
8
9
10
11
12
13
//方法通道的方法是异步的
Future<Null> _getBatteryLevel() async {
String batteryLevel;
try {
final int result = await methodChannel.invokeMethod('getBatteryLevel');
batteryLevel = 'Battery level $result .';
} on PlatformException catch (e) {
batteryLevel = 'Battery level unknown ${e.message}';
}
setState(() {
_batteryLevel = batteryLevel;
});
}

BasicMessageChannel(互相发送消息)

Android给Flutter发消息

Android

初始化BasicMethodChannel

1
BasicMessageChannel messageChannel = new BasicMessageChannel<>(flutterView, "messageChannel", StandardMessageCodec.INSTANCE);

调用发送消息的方法

1
2
3
4
5
private void sendMessageToFlutter() {
if (this.mBasicMessageChannel != null) {
this.mBasicMessageChannel.send("Message From Native");
}
}
Flutter

初始化BasicMessageChannel

1
static const basicMessageChannel = BasicMessageChannel('messageChannel', StandardMessageCodec());

添加接收信息处理方法

1
2
3
void _listenMessageFromNative() {
basicMessageChannel.setMessageHandler(_receiveMessageFromNative);
}

处理接收的数据

1
2
3
4
5
6
//Flutter接收Native发来的消息
Future<dynamic> _receiveMessageFromNative(Object result) async {
setState(() {
_messageFromNative = result.toString();
});
}

Flutter给Android发消息

Android:

初始化BasicMessageChannel并添加plugin给handler

1
2
BasicMessageChannel messageChannel = new BasicMessageChannel<>(flutterView, "messageChannel", StandardMessageCodec.INSTANCE);
messageChannel.setMessageHandler(plugin);

plugin实现BasicMessageChannel.MessageHandler接口的onMessage方法,处理接收到的信息

1
2
3
4
5
public void onMessage(Object o, BasicMessageChannel.Reply reply) {
Toast.makeText(mContext, o.toString(), Toast.LENGTH_LONG).show();
reply.reply(o.toString()+" back from native");
}
//reply返回数据给Flutter
Flutter

初始化BasicMessageChannel

1
static const basicMessageChannel = BasicMessageChannel('messageChannel', StandardMessageCodec());

发送消息给Android并接收返回数据

1
2
3
4
5
6
7
8
Future<dynamic> _sendMessageToNative(String message) async {
String reply = await basicMessageChannel.send(message);
print(reply);
setState(() {
_replayFromNative = reply;
});
return reply;
}

EventChannel(原生发送消息,Flutter接收)

Android:

初始化EventChannel并添加plugin给handler

1
2
EventChannel eventChannel = new EventChannel(flutterView, EVENT_CHANNEL);
eventChannel.setStreamHandler(plugin);

plugin实现EventChannel.StreamHandler接口及onListen、onCancel方法

在onListen中通过EventChannel.EventShink的实例发消息给Flutter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public void onListen(Object o, EventChannel.EventSink eventSink) {
BroadcastReceiver chargingBroadcastReceiver = createChargingBroadcaseReceiver(eventSink);
mContext.registerReceiver(chargingBroadcastReceiver,new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
}
@Override
public void onCancel(Object o) {
}
private BroadcastReceiver createChargingBroadcaseReceiver(EventChannel.EventSink eventSink) {
return new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
int status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, -1);
if (status == BatteryManager.BATTERY_STATUS_UNKNOWN) {
eventSink.error("UNAVALIABLE", "charging status is unavailable", null);
} else {
boolean isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING;
eventSink.success(isCharging ? "charging" : "disCharging");
}
}
};
}

Flutter

初始化EventChannel

1
static const _eventChannel = const EventChannel('charging');

添加接收数据方法

1
2
3
4
5
6
7
8
9
10
void listenNativeEvent() {
_eventChannel.receiveBroadcastStream().listen(_onEvent, onError: _onError);
}
//接收返回的数据
void _onEvent(Object object) {
String s = "Battery is ${object == 'charging' ? '' : 'dis'}Charging";
setState(() {
_batteryStatus = s;
});
}

桥接View给Flutter使用

Android

自定义View,继承自PlatformView

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class MyTextview implements PlatformView {
TextView t;
public MyTextview(Context context, MessageCodec<Object> messenger, int id, Map<String, Object> params){
TextView textView = new TextView(context);
//获取参数,是否有传递参数过来
if (params.containsKey("text")){
textView.setText(params.get("text").toString());
}
this.t = textView;
}
@Override
public View getView() {
return t;
}

@Override
public void dispose() {
}
}

实现PlatformViewFactory

1
2
3
4
5
6
7
8
9
10
11
12
public class TextViewFactory extends PlatformViewFactory {
private MessageCodec<Object> messageCodec;
public TextViewFactory(MessageCodec<Object> createArgsCodec) {
super(createArgsCodec);
this.messageCodec = createArgsCodec;
}

@Override
public PlatformView create(Context context, int i, Object o) {
return new MyTextview(context, messageCodec, i, (Map<String, Object>) o);
}
}

注册View给Flutter使用

1
2
registrar.platformViewRegistry().registerViewFactory("TextView", new TextViewFactory(new StandardMessageCodec()));
//起名叫TextView,给Flutter用做viewType

Flutter

使用桥接的View

1
2
3
4
5
AndroidView(
viewType: 'TextView',
creationParams: {'text': 'TTTeeeXXXttt'},
creationParamsCodec: new StandardMessageCodec(),
),//其中creationParams,creationParamsCodec必须同时存在或不存在

插件的使用

在yaml文件和其他dependencies一样使用。

Git packages(远端)

代码上传到Git,并打一个tag

yaml文件引用

1
2
3
4
5
dependencies:
flutter_remote_package:
git:
url: git@gitlab....
ref: 0.0.1 //可以是commit、branch、tag

本地

在Flutter App根目录下创建plugins文件夹,把插件移动到plugins下。

1
2
3
dependencies:
flutter_plugin_batterylevel:
path: plugins/flutter_plugin_batterylevel

以上限于在创建工程的时候,使用的是plugins创建的,有时候会在自己的Android或iOS工程内部开发,就不这么方便分离发布了。

有时候需要到UI thread执行channelMethod,在Android上需要post一个Runnable到Android UI线程。

1
2
3
4
5
6
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run(){
// call the desired channel message here.
}
})
  • 所谓的“传View”的本质是传递纹理ID,我们只需要明白Flutter是通过Presentation实现了外接纹理,在创建Presentation时,传入FlutterView对应的Context和创建出来的一个虚拟显示屏对象,使得Flutter可以直接通过ID找到并使用Native创建出来的纹理数据。
  • 事件处理,从Native传递到Flutter这一阶段Flutter按照自己的规则处理事件,如果AndroidView获取到了事件,事件会被封装成相应的Native端的事件通过方法通道传回Native,Native再处理事件。
    对于可能出现的滑动时间冲突,可以参考官方注释:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/// 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].
///
/// GestureDetector(
/// onVerticalDragStart: (DragStartDetails d) {},
/// child: AndroidView(
/// viewType: 'webview',
/// gestureRecognizers: <OneSequenceGestureRecognizer>[],
/// ),
/// )
///
/// To get the [AndroidView] to claim the vertical drag gestures we can pass a vertical drag gesture recognizer in [gestureRecognizers] e.g:
///
/// GestureDetector(
/// onVerticalDragStart: (DragStartDetails d) {},
/// child: SizedBox(
/// width: 200.0,
/// height: 100.0,
/// child: AndroidView(
/// viewType: 'webview',
/// gestureRecognizers: <OneSequenceGestureRecognizer>[new VerticalDragGestureRecognizer()],
/// ),
/// ),
/// )

发布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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
name: Publish to Pub.dev

# 流程触发时机,当有标签创建时触发,如 v1.0.0。当然也可以选择别的触发时机,如 push,release 等
on: create

jobs:
publishing:
runs-on: ubuntu-latest
steps:
# 拉去仓库代码
- name: "Checkout"
uses: actions/checkout@v2
# 发布插件
- name: Dart and Flutter Package Publisher
uses: k-paxian/dart-package-publisher@v1.2
with:
# 设置发布插件需要的 Token
accessToken: ${{ secrets.OAUTH_ACCESS_TOKEN }}
refreshToken: ${{ secrets.OAUTH_REFRESH_TOKEN }}

流程中需要设置 OAUTH_ACCESS_TOKEN 和 OAUTH_REFRESH_TOKEN 这两个 Token,他们在 .pub-cache/credentials.json 的文件内,这个文件是第一次手动发布插件成功后自动生成的,在用户的 home 目录或者是安装 Flutter SDK 目录内。

拿到 Token 后去插件仓库添加以上两个 Secret,至此配置工作已完成

发布插件

现在每次更新插件只需要新增标签然后推送到仓库,就可以自动更新插件啦!

1
2
git tag v1.0.1
git push --tags

下面以 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 平台代码待实现类

    1
    2
    ./android/src/main/java/com/hello/hello/InstallPlugin.java
    ./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
    36
    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 平台代码待实现类

    1
    2
    /ios/Classes/HelloPlugin.m
    /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) {...}
    }

编写单元测试

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
29
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);
});
}