1、初识 Flutter Plugin
Flutter 的插件类似于我们在 Android 中说的第三方库,通过使用插件,可以借助插件中的代码实现一些额外功能。
Flutter 的插件以 package 的形式存在,使用 package 的目的是为了达到模块化,可以让代码被共享和复用。这些 package 可以直接在 pubspec.yaml 中被依赖。
一个 package 至少包含两个部分:
- pubspec.yaml 文件:元数据文件,声明了 package 的名称、版本、作者等信息
- lib 文件夹:包含 package 的公开代码,至少会存在
<pakcage-name>.dart
这个文件,该文件用于使用者快速 import 这个 package 以便使用代码内容,因此必须存在
package 的种类分为两种:
- Dart packages:纯 Dart 代码的 package。它包含 Flutter 的特定功能,因此它依赖于 Flutter Framework,只能用在 Flutter 上
- Plugin packages:包含 Dart 代码编写的 API,也包含平台(Android/iOS)特定实现的 package,可以被 Android 和 iOS 调用
2、认识插件结构
2.1 通过源码了解插件工作原理
我们创建一个 Flutter 插件项目,它的目录结构如下:
关注其中的三个目录:
- android:Android 原生代码,相对于 Flutter 而言,就是 Native 代码
- example:AS 生成的使用 Flutter 插件的示例项目
- lib:Flutter 插件源码
在 /example/lib/ 目录下有一个 main.dart 文件是示例程序的入口,在页面中央会展示平台版本 _platformVersion
,该变量通过插件内定义的 getPlatformVersion() 获取:
dart
class _MyAppState extends State<MyApp> {
String _platformVersion = 'Unknown';
final _pluginDemoPlugin = PluginDemo();
@override
void initState() {
super.initState();
initPlatformState();
}
Future<void> initPlatformState() async {
String platformVersion;
try {
platformVersion =
await _pluginDemoPlugin.getPlatformVersion() ?? 'Unknown platform version';
} on PlatformException {
platformVersion = 'Failed to get platform version.';
}
if (!mounted) return;
setState(() {
_platformVersion = platformVersion;
});
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('Plugin example app'),
),
body: Center(
child: Text('Running on: $_platformVersion\n'),
),
),
);
}
}
插件 PluginDemo 通过 AS 创建的模板代码生成:
dart
import 'plugin_demo_platform_interface.dart';
class PluginDemo {
Future<String?> getPlatformVersion() {
return PluginDemoPlatform.instance.getPlatformVersion();
}
}
插件在这里进行了一层封装,需要再进一步看 PluginDemoPlatform:
dart
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
import 'plugin_demo_method_channel.dart';
abstract class PluginDemoPlatform extends PlatformInterface {
PluginDemoPlatform() : super(token: _token);
static final Object _token = Object();
// _instance 是一个 MethodChannelPluginDemo
static PluginDemoPlatform _instance = MethodChannelPluginDemo();
// instance 的 getter 和 setter
static PluginDemoPlatform get instance => _instance;
static set instance(PluginDemoPlatform instance) {
PlatformInterface.verifyToken(instance, _token);
_instance = instance;
}
// 默认的方法没有提供实现
Future<String?> getPlatformVersion() {
throw UnimplementedError('platformVersion() has not been implemented.');
}
}
PluginDemoPlatform.instance
实际上是子类对象 MethodChannelPluginDemo,创建一个 MethodChannel 来执行 getPlatformVersion():
dart
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'plugin_demo_platform_interface.dart';
/// An implementation of [PluginDemoPlatform] that uses method channels.
class MethodChannelPluginDemo extends PluginDemoPlatform {
/// The method channel used to interact with the native platform.
@visibleForTesting
final methodChannel = const MethodChannel('plugin_demo');
@override
Future<String?> getPlatformVersion() async {
final version = await methodChannel.invokeMethod<String>('getPlatformVersion');
return version;
}
}
MethodChannel 需要输入一个名字作为参数:
dart
class MethodChannel {
// name 是 Channel 的名字
// codec 是消息编解码器
const MethodChannel(this.name, [this.codec = const StandardMethodCodec(), BinaryMessenger? binaryMessenger ])
: _binaryMessenger = binaryMessenger;
}
MethodChannel 这里我们要注意两个参数:
- name:String 类型,Channel 的名字,用于区分 Channel,因此必须独一无二
- codec:消息编解码器,Flutter 插件的作用包含在 Flutter 应用与各个 Platform(Android/iOS)之间进行通信,比如方法调用与数据传递都是通信的一部分,那么这个通信过程可以看作协议,发送方需要通过编码器对协议内容进行编码,接收方需要通过解码器对协议内容进行解码
这里我们是从源码角度来看 Channel,后续我们也会再从架构角度对 Platform Channel 进行解析,还会再看到相关内容。
然后在执行 invokeMethod() 时会调用 Native(Flutter 的 Native 就是 Android 或 iOS)的插件源码,比如 Android 端的 Flutter/plugin_demo/android/src/main/kotlin/<PackageName>/PluginDemoPlugin.kt
:
kotlin
class PluginDemoPlugin: FlutterPlugin, MethodCallHandler {
private lateinit var channel : MethodChannel
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "plugin_demo")
channel.setMethodCallHandler(this)
}
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
if (call.method == "getPlatformVersion") {
result.success("Android ${android.os.Build.VERSION.RELEASE}")
} else {
result.notImplemented()
}
}
override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
channel.setMethodCallHandler(null)
}
}
onMethodCall() 内在被调用的方法名是 getPlatformVersion 时会返回一个字符串,标记出 Android 的系统版本。
需要注意的是,在 Flutter 插件的 MethodChannelPluginDemo 内创建 MethodChannel 时传入的名字(plugin_demo),需要与 Native 这边创建的 MethodChannel 传入的名字一致,才可成功调用方法。
2.2 实现一个简单的插件
熟悉了插件结构,就可以开发一个简单的插件了。比如 Flutter 应用中打印 log 只能通过 print() 或 debugPrint() 打印,默认都是 Info 等级,使用起来并不友好。因此我们可以通过 Flutter 插件提供打印各种日志等级的方法,具体操作过程如下:
-
Native 端:修改 Android 端的 PluginDemoPlugin,在 onMethodCall() 中添加输出 Error 级别 Log 的方法:
kotlinoverride fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { if (call.method == "getPlatformVersion") { result.success("Android ${android.os.Build.VERSION.RELEASE}") } else if (call.method == "logE") { val tag = call.argument("tag"); val msg = call.argument("message"); android.util.Log.e(tag, msg); } else { result.notImplemented() } }
-
Flutter 插件:
-
先在插件提供的平台基类 PluginDemoPlatform 中提供 logE 的默认实现:
dartFuture<void> logE(String tag, String message) { throw UnimplementedError('logE() has not been implemented.'); }
-
然后在 PluginDemoPlatform 的子类 MethodChannelPluginDemo 中提供 logE 的实现,通过 MethodChannel 调用 Native 的方法:
dart@override Future<void> logE(String tag, String message) async { // arguments 是 dynamic 的,因为我们要传两个参数,因此做成一个 Map await methodChannel .invokeMethod<String>('logE', {'tag': tag, 'message': message}); }
-
最后在 PluginDemo 中提供一个封装方法:
dartvoid logE(String tag, String message) { PluginDemoPlatform.instance.logE(tag, message); }
-
-
example 示例:在示例的 main 文件中通过插件调用 logE():
dartclass _MyAppState extends State<MyApp> { final _pluginDemoPlugin = PluginDemo(); @override void initState() { super.initState(); initPlatformState(); // 通过插件调用输入 Error 等级 log 的方法 _pluginDemoPlugin.logE('FlutterPlugin', 'init work'); } }
3、Plugin 通信原理
介绍 Plugin 之前需要先了解一下 Flutter 框架。
3.1 Flutter 框架
Flutter 框架包括 Framework 和 Engine,他们运行在各自的 Platform 上:
Framework 由 Dart 语言开发,包含 Material Design 风格(Android)和 Cupertino 风格(iOS)的 Widget,还有文本、图片、按钮等基础 Widget;此外,还包括渲染、动画、绘制、手势等基础能力。
Engine 是 C++ 实现的,包括 Skia(二维图形库)、Dart VM(Dart Runtime)、Text(文本渲染)等。Flutter 的上层能力实际上是 Engine 提供的。通过 Engine 将各个 Platform 的差异抹平,Flutter Plugin 就是通过 Engine 提供的 Platform Channel 实现的通信。
Flutter 插件调用 Android Native 代码的过程,实际上就是从 Framework 到 Engine,Engine 到 Android 底层再向上到 Java/Kotlin 的 U 型结构。
3.2 Platform Channel
Flutter Plugin 本质上是一个特殊的 package,它提供了 Android 或 iOS 的底层封装,在 Flutter 层提供组件功能,使得 Flutter 可以方便的调取 Native 代码。
很多平台相关性或者对于 Flutter 实现起来比较复杂的部分,都可以封装成 Plugin,其原理图如下:
图中可见,Flutter app 通过 Plugin 创建的 Platform Channel 调用 Native API,下面来详细介绍 Platform Channel 的各个部分。
还是先看上面的图,Flutter app 作为 client,通过 MethodChannel 向其他 Platform(Android/iOS)发送调用消息,而 Android Platform 作为 Host 通过 MethodChannel 接收调用消息,而另一个 Host iOS Platform 则通过 FlutterMethodChannel 接收调用消息。
Android Platform 中有一个 FlutterActivity 作为 Android Plugin 的管理器,它记录了所有 Plugin 并将它们绑定到 FlutterView 中。
Flutter 提供了三种不同类型的 Channel:
- BasicMessageChannel:用于传递字符串和半结构化的信息
- MethodChannel:用于传递方法调用(method invocation),方法调用也可以反向发送调用消息
- EventChannel: 用于数据流(event streams)的通信
三种 Channel 相互独立,各有用途,但是在设计上非常相近,均有三个重要成员变量:
- name: String 类型,代表 Channel 的名字,也是其唯一标识符。Flutter 应用可能会有多个 Channel,它们通过 name 区分,因此每个 Channel 必须指定一个独一无二的名字。并且,消息从 Flutter 发送到 Platform 时,在 Platform 这边也要通过传递过来的 channel name 找到该 Channel 对应的 Handler(消息处理器)
- messager:BinaryMessenger 类型,代表消息信使,是消息的发送与接收的工具
- codec: MessageCodec 类型或 MethodCodec 类型,代表消息的编解码器。消息编解码器,是 JSON 格式的二进制序列化,所以调用方法的参数类型必须是可 JSON 序列化的。
平台之间传递的数据必须是支持二进制序列化的,否则无法通过消息编解码器传递消息。标准平台通道使用标准消息编解码器,以支持简单的类似 JSON 值的高效二进制序列化,例如 booleans、numbers,、Strings,、byte buffers,、List,、Maps(详细信息参考 StandardMessageCodec)。 当发送和接收值时,这些值在消息中的序列化和反序列化会自动进行。
下表显示了如何在宿主上接收 Dart 值,反之亦然:
Dart | Android | iOS |
---|---|---|
null | null | nil (NSNull when nested) |
bool | java.lang.Boolean | NSNumber numberWithBool: |
int | java.lang.Integer | NSNumber numberWithInt: |
int, if 32 bits not enough | java.lang.Long | NSNumber numberWithLong: |
int, if 64 bits not enough | java.math.BigInteger | FlutterStandardBigInteger |
double | java.lang.Double | NSNumber numberWithDouble: |
String | java.lang.String | NSString |
Uint8List | byte[] | FlutterStandardTypedData typedDataWithBytes: |
Int32List | int[] | FlutterStandardTypedData typedDataWithInt32: |
Int64List | long[] | FlutterStandardTypedData typedDataWithInt64: |
Float64List | double[] | FlutterStandardTypedData typedDataWithFloat64: |
List | java.util.ArrayList | NSArray |
Map | java.util.HashMap | NSDictionary |
4、开发步骤总结
4.1 开发 Dart package
AS 创建新的 Flutter 项目,Project type 选择 Package,或者通过 flutter create
命令,将 template 参数指定为 package 创建:
shell
flutter create --template=package hello
生成的 Package 模板结构如下:
lib/hello.dart
:Package 的 Dart 代码test/hello_test.dart
:Package 的单元测试代码- 实现 Package:
- 对于纯 Dart 包,需要在
lib/<package name>.dart
文件内或在 lib 下添加新文件实现 - 对于测试包,在 test 目录中添加单元测试代码
- 对于纯 Dart 包,需要在
4.2 开发 Plugin package
创建 Plugin
AS 创建新的 Flutter 项目,Project type 选择 Plugin,或者通过 flutter create
命令,将 template 参数指定为 plugin,同时将 org 参数使用反向域名表示法指定组织名并创建:
shell
flutter create --org com.example --template=plugin hello
生成的 Plugin 模板结构如下:
-
lib/hello.dart
:插件包的 Dart API -
android/src/main/java/com/<yourcompany>/hello/HelloPlugin.java
:插件包 API 的 Android 实现代码 -
ios/Classes/HelloPlugin.m
:插件包 API 的 iOS 实现代码 -
example 目录:依赖于 Plugin 的示例程序。默认情况下,示例代码在 Android 和 iOS 上分别使用 Java 和 Objective-C,如果想使用 Kotlin 或 Swift,可以用过 -i 或 -a 参数指定语言:
shellflutter create --template=plugin -i swift -a kotlin hello
实现 Plugin
因为 Plugin 是包含多个平台的代码的,因此实现 Plugin 需要在多个平台上联动:
- 用 Dart 语言实现 Flutter Plugin(代码位置在根目录的 lib 文件夹下)
- 用 Kotlin/Java 实现 Android 原生代码(代码位置在根目录下的 /android/src/main 文件夹下)
- 用 Objective-C/Swift 实现 iOS 原生代码(代码位置在根目录下的 ios 文件夹下)
编辑 Android 代码之前需要确保代码至少构建过一次,可以在 AS 的终端执行 cd 命令进入到示例代码的目录,然后用 flutter 命令进行 apk 构建:
shell
cd hello/example
flutter build apk
最后需要通过 Platform Channel 将 Dart 代码与平台相关的特定实现连接起来,这一部分在认识插件结构一节中已经讲过。
添加文档
建议将以下文档添加到所有软件包:
- README.md:介绍包的文件
- CHANGELOG.md:记录每个版本中的更改
- LICENSE:包含软件包许可条款的文件
4.3 发布 package
实现完毕的 package 可以在 Pub 上发布供他人使用。发布的步骤如下:
-
发布前,需要先检查
pubspec.yaml
、README.md
以及CHANGELOG.md
文件,以确保其内容的完整性和正确性 -
运行 dry-run 命令查看是否都准备 OK 了(如果有问题需要根据错误信息修改):
shellflutter packages pub publish --dry-run
-
最后,运行发布命令:
shellflutter packages pub publish
发布 package 需要使用 Google 账号,在运行命令时可能会因为网络环境延迟或失败。