目标:
1.Flutter插件是什么?有什么作用?
插件 (plugin) 是 package 的一种,全称是 plugin package,我们简称为 plugin,中文叫插件。
2.怎么创建Flutter插件?
一、什么是插件
在flutter中,一个插件叫做一个package,使用packages的目的就是为了达到模块化,可以创建出可被复用和共享的代码,这和大多数编程语言中的模块、包的概念相同。创建出来的package可以在pubspec.yaml中直接依赖。
1.1 package组成
Flutter插件组成
- 一个pubspec.yaml文件:一个元数据文件,声明了声明了package的名称、版本、作者等信息。
- 一个lib文件夹:包含里package的公开代码,文件夹至少需要存在<pakcage-name>.dart这个文件。
注意:<pakcage-name>.dart这个文件必须存在,因为这是方便使用的人快速import这个package来使用它,可以把它理解成一种必须要遵守的规则。
1.2 package分类
package可以分为两种:纯dart代码的package和带有特定平台代码的package。
- Dart packages:这是一个只有dart代码的package,里面包含了flutter的特定功能,所以它依赖于flutter的framework,也决定了它只能用在flutter上。
- plugin packages:这是一个既包含了dart代码编写的api,又包含了平台(Android/IOS)特定实现的package,可以被Android和ios调用。
- FFI 插件
用 Dart 语言编写针对一个或多个特定平台的 API,使用 Dart FFI (Android、iOS、macOS)。
> 上面应该很好理解,可以理解成java jar包和Android sdk的区别。而要开发的日志插件就是第二种。
二、插件开发
2.1 创建package
可以使用AS创建插件
然后点击next。
然后点击 Create按钮,开始创建插件项目。
如果是采用Flutter命令创建项目
// 想要创建初始的 Flutter package,请使用带有 --template=package 标志的 flutter create 命令:flutter create --template=package hello
2.2 项目文件结构
项目文件结构如下:
LICENSE 文件
大概率会是空的一个许可证文件。
- test/hello_test.dart 文件
Package 的 单元测试 文件。
- hello.iml 文件
由 IntelliJ 生成的配置文件。
- .gitignore 文件
告诉 Git 系统应该隐藏哪些文件或文件夹的一个隐藏文件。
- .metadata 文件
IDE 用来记录某个 Flutter 项目属性的的隐藏文件。
- pubspec.yaml 文件
pub 工具需要使用的,包含 package 依赖的 yaml 格式的文件。
- README.md 文件
起步文档,用于描述 package。
- lib/hello.dart 文件
package 的 Dart 实现代码。
- .idea/modules.xml、.idea/workspace.xml 文件
IntelliJ 的各自配置文件(包含在 .idea 隐藏文件夹下)。
- CHANGELOG.md 文件
又一个大概率为空的文档,用于记录 package 的版本变更。插件的native端实现
- android/
插件包API的Android实现
- iOS
插件包API的ios实现.
- example/:
一个依赖于该插件的Flutter应用程序,来说明如何使用它
lib库定义插件的主要功能。
2.3 实现插件
对于纯 Dart 库的 package,只要在 lib/<package name>.dart 文件中添加功能实现,或在 lib 目录中的多个文件中添加功能实现。
如果要对 package 进行测试,在 test 目录下添加 单元测试。
2.3.1 创建MethodChannel
项目默认生成了插件MethodChannel
1. 创建MethodChannel
flutter_log_plugin_method_channel.dart
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';import 'flutter_log_plugin_platform_interface.dart';/// An implementation of [FlutterLogPluginPlatform] that uses method channels.
class MethodChannelFlutterLogPlugin extends FlutterLogPluginPlatform {/// The method channel used to interact with the native platform.@visibleForTestingfinal methodChannel = const MethodChannel('flutter_log_plugin');@overrideFuture<String?> getPlatformVersion() async {final version = await methodChannel.invokeMethod<String>('getPlatformVersion');return version;}
}
2.定义插件方法
- 创建了一个MethodChannel,名称为flutter_log_plugin
- 提供了一个方法访问getPlatformVersion
我们看看其调用的方式,通过创建的methodChannel.invokeMethod来调用原生实现。
final version = await methodChannel.invokeMethod<String>('getPlatformVersion');
2.3.2 插件方法Native实现
在项目 android 目录下,增加对 MethodChannel 方法的实现。
默认的插件实现的功能:Dart通过插件,获取 native端系统版本信息。
在android/src.main 下,实现了Native方法
package com.example.flutter_log_pluginimport androidx.annotation.NonNullimport io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result/** FlutterLogPlugin */
class FlutterLogPlugin: FlutterPlugin, MethodCallHandler {/// The MethodChannel that will the communication between Flutter and native Android////// This local reference serves to register the plugin with the Flutter Engine and unregister it/// when the Flutter Engine is detached from the Activityprivate lateinit var channel : MethodChanneloverride fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {channel = MethodChannel(flutterPluginBinding.binaryMessenger, "flutter_log_plugin")channel.setMethodCallHandler(this)}/*** 方法调用处理** @author zhouronghua* @time 2024/6/27 下午3:12*/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)}
}
MethodChannel提供Flutter与原生系统之间的通信。
1.绑定MethodChannel: Activity attach到Flutter引擎
/// This local reference serves to register the plugin with the Flutter Engine and unregister it/// when the Flutter Engine is detached from the Activityprivate lateinit var channel : MethodChanneloverride fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {channel = MethodChannel(flutterPluginBinding.binaryMessenger, "flutter_log_plugin")channel.setMethodCallHandler(this)}
因为Flutter提供的引擎是“flutter_log_plugin”名称,因此通过 命名找到对应的MethodChannel。
这样Activity就可以绑定Flutter MethodChannel,可以建立通信通道。
设置MethodCallHandler,注册插件到Flutter引擎。
channel.setMethodCallHandler(this)
2. Flutter调用Native方法
建立通道以后,Flutter调用Native端方法,方法名为getPlatformVersion,没有参数。
/*** 方法调用处理** @author zhouronghua* @time 2024/6/27 下午3:12*/override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {if (call.method == "getPlatformVersion") {result.success("Android ${android.os.Build.VERSION.RELEASE}")} else {result.notImplemented()}}
Native端接收方法调用的入口是 onMethodCall
- 首先匹配方法名
- 根据call的参数进行处理
- 返回方法调用结果,通过result保存结果值。
- 如果对应名称的方法未实现,则设置 result.notImplented()
当前获取安卓系统版本,返回结果是
"Android ${android.os.Build.VERSION.RELEASE}"
3.Activity与Flutter引擎断开时注销插件
override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {channel.setMethodCallHandler(null)}
2.3.3 实现日志打印插件
1.声明接口方法
在lib插件的接口文件flutter_log_plugin_platform_interface.dart
声明接口方法 logI
/*** 声明接口方法** @author zhouronghua* @time 2024/6/27 下午4:05*/void logI(String tag, String message) {throw UnimplementedError('logI() has not been implemented.');}
2. 插件端定义日志打印方法实现 logI
/*** 日志打印:I级别* 说明: 日志打印不需要接收结果,因此不需要异步回调** @author zhouronghua* @time 2024/6/27 下午3:45*/@overridevoid logI(String tag, String message) {/// 调用原生方法logI, 参数集为 {tag, message}/// 参数集按照键值对传递methodChannel.invokeMethod('logI', {"tag": tag, "message": message});}
调用的Native段方法名为 “logI”,对应的参数为:
{"tag": tag, "message": message}
参数一般使用键值对进行传递,参数之间采用逗号分隔。
注意,此处一定要使用注解 @override,否则调用logI编译报错
3.Native端实现方法接收处理
在android/src.main下,FlutterLogPlugin增加日志方法调用的实现。
/*** 方法调用处理** @author zhouronghua* @time 2024/6/27 下午3:12*/override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {if (call.method == "getPlatformVersion") {// 获取系统版本信息result.success("Android ${android.os.Build.VERSION.RELEASE}")} else if (call.method == "logI") {// 日志打印处理 logI(参数要与插件的键保持一致)final String tag = call.argument("tag")final String message = call.argument("message")android.util.Log.i(tag, message)} else {result.notImplemented()}}
4.Flutter插件入口添加日志打印方法
FlutterLogPlugin中,增加日志打印入口调用
/*** Flutter插件入口* 门面模式** @author zhouronghua* @time 2024/6/27 下午4:11*/
class FlutterLogPlugin {Future<String?> getPlatformVersion() {return FlutterLogPluginPlatform.instance.getPlatformVersion();}/*** 日志打印调用** @author zhouronghua* @time 2024/6/27 下午4:11*/void logI(String tag, String message) {return FlutterLogPluginPlatform.instance.logI(tag, message);}}
这个是典型的门面模式,外部调用的使用不需要关注Flutter插件内部实现细节。
5.测试日志打印方法
在example/lib下,main.dart中,测试日志打印方法调用。
// Platform messages are asynchronous, so we initialize in an async method.Future<void> initPlatformState() async {// 调用日志打印_flutterLogPlugin.logI("MyApp", "开始初始化平台");String platformVersion;// Platform messages may fail, so we use a try/catch PlatformException.// We also handle the message potentially returning null.try {platformVersion = await _flutterLogPlugin.getPlatformVersion() ??'Unknown platform version';} on PlatformException {platformVersion = 'Failed to get platform version.';}// 调用日志打印_flutterLogPlugin.logI("MyApp", "平台信息是:$platformVersion");// If the widget was removed from the tree while the asynchronous platform// message was in flight, we want to discard the reply rather than calling// setState to update our non-existent appearance.if (!mounted) return;setState(() {_platformVersion = platformVersion;});}
问题一:编译报错logI未实现
还是报错
Restarted application in 1,896ms.
E/flutter ( 2023): [ERROR:flutter/lib/ui/ui_dart_state.cc(198)] Unhandled Exception: MissingPluginException(No implementation found for method logI on channel flutter_log_plugin)
E/flutter ( 2023): #0 MethodChannel._invokeMethod (package:flutter/src/services/platform_channel.dart:165:7)
E/flutter ( 2023): <asynchronous suspension>
E/flutter ( 2023):
E/flutter ( 2023): [ERROR:flutter/lib/ui/ui_dart_state.cc(198)] Unhandled Exception: MissingPluginException(No implementation found for method logI on channel flutter_log_plugin)
E/flutter ( 2023): #0 MethodChannel._invokeMethod (package:flutter/src/services/platform_channel.dart:165:7)
E/flutter ( 2023): <asynchronous suspension>
E/flutter ( 2023):
test模块,flutter_log_plugin_test.dart缺少对应的logI方法的实现,因此报错。添加
class MockFlutterLogPluginPlatform with MockPlatformInterfaceMixinimplements FlutterLogPluginPlatform {@overrideFuture<String?> getPlatformVersion() => Future.value('42');@overridevoid logI(String tag, String message) {debugPrint("tag=$tag message=$message");}
}
6.打包apk
Terminal中,编译安卓APK。
$ cd example/android$ gradlew clean$ flutter build apk
如果报错,修正报错信息,重新打包试试。
出现下面的信息则编译成功。
安装运行APK,看看是否打印日志信息
能够输出对应的日志信息。
三、插件原理
Plugin其实就是一个特殊的Package。Flutter Plugin提供Android或者iOS的底层封装,在Flutter层提供组件功能,使Flutter可以较
方便的调取Native的模块。很多平台相关性或者对于Flutter实现起来比较复杂的部分,都可以封装成Plugin。
3.1 Platform Channel
Platform Channel:
1. Flutter App (Client),通过MethodChannel类向Platform发送调用消息;
2. Android Platform (Host),通过MethodChannel类接收调用消息;
3. iOS Platform (Host),通过FlutterMethodChannel类接收调用消息。
- > PS:消息编解码器,是JSON格式的二进制序列化,所以调用方法的参数类型必须是可JSON序列化的。
- > PS:方法调用,也可以反向发送调用消息。
3.2 安卓平台
FlutterActivity,是Android的Plugin管理器,它记录了所有的Plugin,并将Plugin绑定到FlutterView。
3.3 理解Platform Channel工作原理
Flutter定义了三种不同类型的Channel,它们分别是
- BasicMessageChannel:用于传递字符串和半结构化的信息。
- MethodChannel:用于传递方法调用(method invocation)。
- EventChannel: 用于数据流(event streams)的通信。
三种Channel之间互相独立,各有用途,但它们在设计上却非常相近。每种Channel均有三个重要成员变量:
- name: String类型,代表Channel的名字,也是其唯一标识符。
- messager:BinaryMessenger类型,代表消息信使,是消息的发送与接收的工具。
- codec: MessageCodec类型或MethodCodec类型,代表消息的编解码器。
- Channel name
一个Flutter应用中可能存在多个Channel,每个Channel在创建时必须指定一个独一无二的name,Channel之间使用name来区分彼此。当有消息从Flutter端发送到Platform端时,会根据其传递过来的channel name找到该Channel对应的Handler(消息处理器)。
- 消息信使:BinaryMessenger
- 平台通道数据类型支持和解码器
- 标准平台通道使用标准消息编解码器,以支持简单的类似JSON值的高效二进制序列化,例如 booleans,numbers, Strings, byte buffers, List, Maps(请参阅StandardMessageCodec了解详细信息)。 当您发送和接收值时,这些值在消息中的序列化和反序列化会自动进行。
下表显示了如何在宿主上接收Dart值,反之亦然:
3.4 解码器
消息解码器主要将二进制格式的数据转换为Handler能够识别的数据,Flutter定义了两种Codec:
MessageCodec和MethodCodec。
四、插件打包和发布
4.1 插件检查
一旦完成了 package 的实现,你便可以将其提交到 pub.dev 上,以便其他开发者可以轻松地使用它。
发布你的 package 之前,确保检查了这几个文件:pubspec.yaml
、README.md
和 CHANGELOG.md
,确保它们完整且正确,另外,为了提高 package 的可用性,可以考虑加入如下的内容:
-
代码的示例用法
-
屏幕截图,GIF 动画或者视频
-
代码库的正确指向链接
运行 dry-run 命令以检验是否所有内容都通过了分析:
$ flutter packages pub publish --dry-run
修正提示错误信息。
pubspec.yaml 中anthor字段不需要了,直接删除
修改后再次执行。
Package validation found the following potential issue:
* Packages with an SDK constraint on a pre-release of the Dart SDK should themselves be published as a pre-release version. If this package needs Dart version 2.17.0-239.0.dev, consider publishing the package as a pre-release instead.See https://dart.dev/tools/pub/publishing#publishing-prereleases For more information on pre-releases.Package has 1 warning.
pub finished with exit code 65
4.2 插件发布
最后一步是发布,请注意:发布是永久性 的,运行以下提交命令:
flutter pub publish
设置了中国镜像的开发者们请注意:目前所存在的镜像都不能(也不应该)进行 package 的上传。如果你设置了镜像,执行上述发布代码可能会造成发布失败。网络设定好后,无需取消中文镜像,执行下述代码可直接上传:
flutter pub publish --server=https://pub.dartlang.org
Dart 概览 | Dart