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 插件声明 · 平台接口映射 lib/ Dart 侧 API MethodChannel 调用 android/ Kotlin/Java 实现 MethodCallHandler ios/ Swift/ObjC 实现 FlutterMethodChannel 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 工程中运行,而不是直接在主应用工程中调试。example 的依赖关系最简单,能快速定位是插件本身的问题还是宿主工程配置的问题。

六、平台接口文件的协同关系

把上述内容串起来看,一次完整的插件调用会经过以下路径:

  1. example 应用调用 MyPlugin.getPlatformVersion()。
  2. lib 中的 MethodChannel 以方法名 getPlatformVersion 发起调用。
  3. Android 侧 MyPluginPlugin.onMethodCall 或 iOS 侧对应回调收到调用。
  4. 原生代码执行平台逻辑,通过 result.success(...) 返回结果。
  5. Dart 侧 Future 完成,example 界面更新显示。

整个过程中,pubspec.yaml 决定“谁来接收”,lib 决定“怎么发”,android/ios 决定“怎么处理”,example 决定“怎么验证”。四者缺一不可,任何一处配置或命名不一致,都会导致调用失败。

常见错误:修改了 Dart 侧通道名却忘记同步修改原生侧,或修改了 pluginClass 却未同步更新原生类名。这类问题不会在编译期报错,只会在运行时以 MissingPluginException 的形式暴露,排查时应优先核对这三处命名。

掌握这套结构后,后续无论是新增方法、增加平台,还是排查通道问题,都可以按“pubspec 声明 → lib 转发 → 原生实现 → example 验证”的顺序逐层定位,而不必在多个目录之间盲目翻找。