2. Flutter插件工程结构解析
创建一个 Flutter 插件工程后,最先要做的不是急着写 Dart 代码,而是把目录结构看清楚。插件工程和普通 Flutter 应用工程最大的区别在于:它同时包含 Dart 侧代码和至少一个原生平台侧代码,并且默认带一个 example 应用用于联调。理解每个目录和文件的职责,才能知道“改哪里、为什么改、改完谁生效”。
下面以 flutter create --template=plugin --org com.example --platforms=android,ios my_plugin 生成的工程为例,说明各部分的实际作用。
一、顶层结构总览
插件工程根目录通常包含以下内容:
pubspec.yaml:插件声明文件,定义包名、版本、依赖,以及最关键的flutter.plugin.platforms平台接口映射。lib/:Dart 侧代码,对外暴露插件 API,内部通过 MethodChannel 调用原生实现。android/:Android 原生实现,包含 Gradle 配置和 Kotlin/Java 源码。ios/:iOS 原生实现,包含 podspec 和 Swift/Objective-C 源码。example/:示例应用工程,用于运行、调试和验证插件。test/:Dart 侧单元测试。
其中 pubspec.yaml 是“总入口”,它把 Dart 侧和原生侧连接起来;lib、android、ios 是三个实现层;example 是验证层。四者关系可以用下图表示。
二、pubspec.yaml:插件的“身份证”
普通应用的 pubspec.yaml 只需要 name、environment、dependencies 等字段,而插件必须额外声明 flutter.plugin 段。一个典型的插件声明如下:
name: my_plugin
description: A new Flutter plugin project.
version: 0.0.1
environment:
sdk: '>=3.0.0 <4.0.0'
flutter: ">=3.10.0"
dependencies:
flutter:
sdk: flutter
dev_dependencies:
flutter_test:
sdk: flutter
flutter:
plugin:
platforms:
android:
package: com.example.my_plugin
pluginClass: MyPluginPlugin
ios:
pluginClass: MyPluginPlugin
这里最关键的是 platforms 下的两个条目。android 中的 package 指定原生代码所在的包名,pluginClass 指定 Android 侧入口类名;ios 中的 pluginClass 指定 iOS 侧入口类名。Flutter 工具链在构建时会读取这些信息,自动生成注册代码,把原生实现挂载到引擎上。如果这里写错,最常见的现象是插件在 Dart 侧调用时抛出 MissingPluginException。
三、lib 目录:Dart 侧门面
lib/ 下通常有一个与插件同名的 Dart 文件,例如 my_plugin.dart。它是插件对外的唯一入口,负责:
- 定义公开的 Dart API,例如
Future<String?> getPlatformVersion()。 - 创建并持有
MethodChannel,通道名必须与原生侧完全一致。 - 把 Dart 调用转换为方法名和参数,发送到原生侧,并处理返回结果或异常。
示例代码:
class MyPlugin {
static const MethodChannel _channel =
MethodChannel('com.example.my_plugin/methods');
static Future<String?> getPlatformVersion() async {
final version = await _channel.invokeMethod<String>('getPlatformVersion');
return version;
}
}
注意通道名 com.example.my_plugin/methods 是 Dart 与原生之间的“约定地址”,两端必须逐字符一致。lib 目录不包含任何平台判断逻辑,它只负责转发,这样上层业务代码可以完全不感知 Android 和 iOS 的差异。
四、android 与 ios 目录:原生实现层
android/ 目录下,核心文件是 android/src/main/kotlin/.../MyPluginPlugin.kt。它实现 FlutterPlugin 和 MethodCallHandler 接口,在 onAttachedToEngine 中注册通道,在 onMethodCall 中根据方法名分发处理。Android 侧还需要 build.gradle 声明依赖和编译版本。
ios/ 目录下,核心文件是 ios/Classes/MyPluginPlugin.swift。它遵循 FlutterPlugin 协议,在 register 方法中创建 FlutterMethodChannel 并设置回调。iOS 侧还需要 my_plugin.podspec 描述源码路径、依赖和平台版本。
两端原生代码的职责是对称的:接收方法名、解析参数、执行平台特有逻辑、通过 result 回调返回成功或错误。它们不直接与 Dart 代码耦合,只通过通道名和方法名通信。
五、example 工程:验证与调试的载体
example/ 是一个完整的 Flutter 应用,它的 pubspec.yaml 通过 path: ../ 依赖当前插件。运行 flutter run 时,example 会同时编译 Dart 侧和原生侧代码,因此它是验证插件是否真正可用的最直接方式。
在 example 的 lib/main.dart 中,通常会调用插件 API 并把结果显示在界面上。例如点击按钮后调用 MyPlugin.getPlatformVersion(),界面上显示 “Android 14” 或 “iOS 17.2”。如果显示异常或抛出 MissingPluginException,说明通道名、pluginClass 或平台注册存在问题。
六、平台接口文件的协同关系
把上述内容串起来看,一次完整的插件调用会经过以下路径:
- example 应用调用
MyPlugin.getPlatformVersion()。 - lib 中的
MethodChannel以方法名getPlatformVersion发起调用。 - Android 侧
MyPluginPlugin.onMethodCall或 iOS 侧对应回调收到调用。 - 原生代码执行平台逻辑,通过
result.success(...)返回结果。 - Dart 侧 Future 完成,example 界面更新显示。
整个过程中,pubspec.yaml 决定“谁来接收”,lib 决定“怎么发”,android/ios 决定“怎么处理”,example 决定“怎么验证”。四者缺一不可,任何一处配置或命名不一致,都会导致调用失败。
pluginClass 却未同步更新原生类名。这类问题不会在编译期报错,只会在运行时以 MissingPluginException 的形式暴露,排查时应优先核对这三处命名。
掌握这套结构后,后续无论是新增方法、增加平台,还是排查通道问题,都可以按“pubspec 声明 → lib 转发 → 原生实现 → example 验证”的顺序逐层定位,而不必在多个目录之间盲目翻找。