Flutter 原生插件开发实战指南

35 阅读6分钟

前面写了关于uniapp中定义原生插件,这篇文章介在 Flutter 日常开发中,我们同样会遇到一个尴尬的局面:项目需要一个特定的原生能力,但 pub.dev 上要么没有现成的插件,要么已有的插件年久失修、无法满足需求。这时候,自己动手写一个原生插件就成了唯一的选择。

本文将以一个实际的场景——封装一个原生返回数据的方法——为例,带你走完从创建插件、编写原生代码到调试发布的完整流程。

一、先搞懂:Flutter 和原生到底是怎么“说话”的

在动手之前,有必要先理解 Flutter 与原生通信的底层机制。

Flutter 与 Native 的通信基于 Platform Channel,本质上是一个 C/S 模型:Flutter 端作为 Client,iOS/Android 端作为 Host。Flutter 通过 MethodChannel 向 Native 发送方法调用,Native 收到消息后执行对应的原生代码,再将结果通过 Result 回调返回给 Flutter。

整个调用链路大致是这样的:

  1. Dart 层调用 invokeMethod,方法名和参数被序列化为二进制消息;
  2. 消息通过 BinaryMessenger 发送到 Flutter 引擎的 C++ 层;
  3. 引擎根据 channel 名称查找注册的处理器,在原生主线程异步执行;
  4. 原生代码执行完毕后,通过 result 回调返回数据,原路返回给 Dart 层。

理解了这个流程,后面的开发就只是“填空”而已。

二、创建插件项目:Package 还是 Plugin?

Flutter 生态中有两种“包”,创建方式不同,用途也不同:

  • Dart Package:纯 Dart 代码,比如 http、path,通过 --template=package 创建;
  • Plugin Package:包含原生代码,可以调用 Android/iOS/桌面平台 API,通过 --template=plugin 创建。

我们这次要做的是原生插件,所以使用 plugin 模板。在终端执行:

bash

flutter create --org com.example --template=plugin --platforms=android,ios -a kotlin -i swift my_native_plugin

参数说明:

  • --org:组织标识,采用反向域名表示法,会用于包名和 Bundle ID;
  • --platforms:指定支持的平台,这里选择 Android 和 iOS;
  • -a:Android 开发语言,可选 kotlin 或 java;
  • -i:iOS 开发语言,可选 swift 或 objc。

创建完成后,项目结构大致如下:

text

my_native_plugin/
├── lib/
│   └── my_native_plugin.dart      # Dart API 入口
├── android/                        # Android 原生实现
├── ios/                            # iOS 原生实现
├── example/                        # 调试用的示例 App
└── pubspec.yaml

example 目录非常关键,它相当于插件的“沙盒”,你可以在这里直接运行插件、验证功能,而不需要额外建一个项目来测试。

三、Dart 端:定义你的 API

Flutter 插件的设计模式遵循一个清晰的层次:公开 API 层 → 平台接口层 → 平台实现层。

打开 lib/my_native_plugin.dart,你会看到脚手架已经生成了三个关键文件:

  • my_native_plugin.dart:对外暴露的 API;
  • my_native_plugin_platform_interface.dart:平台接口定义;
  • my_native_plugin_method_channel.dart:基于 MethodChannel 的默认实现。

我们要做的是在这个框架里增加一个自定义方法。假设我们需要一个 getPlatformInfo 方法,返回当前平台信息:

dart

// lib/my_native_plugin.dart
import 'my_native_plugin_platform_interface.dart';

class MyNativePlugin {
  Future<String?> getPlatformInfo() {
    return MyNativePluginPlatform.instance.getPlatformInfo();
  }
}

然后在 platform interface 中声明:

dart

// lib/my_native_plugin_platform_interface.dart
Future<String?> getPlatformInfo() {
  throw UnimplementedError('getPlatformInfo() has not been implemented.');
}

最后在 method channel 实现中完成实际的调用:

dart

// lib/my_native_plugin_method_channel.dart
@override
Future<String?> getPlatformInfo() async {
  final version = await methodChannel.invokeMethod<String>('getPlatformInfo');
  return version;
}

这种分层设计的好处是:如果未来要为某个平台提供纯 Dart 实现,只需要替换 platform interface 的 instance 即可,对外 API 完全不用改。

四、Android 端:Kotlin 实现

Android 原生代码位于 android/src/main/kotlin/.../MyNativePlugin.kt。脚手架已经生成了一个实现了 FlutterPlugin 接口的类,核心逻辑在 onMethodCall 方法中:

kotlin

override fun onMethodCall(call: MethodCall, result: Result) {
    if (call.method == "getPlatformInfo") {
        result.success("Android ${android.os.Build.VERSION.RELEASE}")
    } else {
        result.notImplemented()
    }
}

这里有几个关键点:

  1. 方法名必须与 Dart 端 invokeMethod 的参数完全一致,否则会走到 notImplemented;
  2. result.success() 返回数据给 Dart 端;如果执行出错,使用 result.error();
  3. 如果需要访问 Activity(比如调用 moveTaskToBack 退到桌面),插件类需要实现 ActivityAware 接口,并在 onAttachedToActivity 中获取 Activity 引用。

如果你要开发的是更复杂的 Android 插件,建议用 Android Studio 打开 example/android 目录进行开发,这样能获得完整的代码补全和 Gradle 同步支持。

五、iOS 端:Swift 实现

iOS 原生代码位于 ios/Classes/MyNativePlugin.swift,注册逻辑在 register(with registrar:) 中:

swift

public static func register(with registrar: FlutterPluginRegistrar) {
    let channel = FlutterMethodChannel(name: "my_native_plugin", 
                                       binaryMessenger: registrar.messenger())
    let instance = MyNativePlugin()
    registrar.addMethodCallDelegate(instance, channel: channel)
}

方法处理在 handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) 中:

swift

public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
    switch call.method {
    case "getPlatformInfo":
        result("iOS " + UIDevice.current.systemVersion)
    default:
        result(FlutterMethodNotImplemented)
    }
}

调试 iOS 代码的一个关键技巧:Xcode 不能直接打开插件目录下的 iOS 工程,必须先构建一次示例项目来生成符号链接。在 example 目录下执行:

bash

flutter build ios --no-codesign --config-only

然后用 Xcode 打开 example/ios/Runner.xcworkspace,在 Project Navigator 中通过 Pods/Development Pods/my_native_plugin/../../example/ios/.symlinks/plugins/my_native_plugin/ios/Classes 路径找到插件源码。

六、运行与调试

直接在 example 目录下运行项目:

bash

cd example
flutter run

示例 App 的 main.dart 中调用插件方法,你可以在控制台看到输出。建议在开发过程中充分利用 flutter run 的热重载能力——虽然原生代码的修改需要重新构建,但 Dart 层的调整可以即时生效。

调试原生代码时,Android 端可以使用 Android Studio 的 Debugger 附加到 Flutter 进程;iOS 端则在 Xcode 中正常断点调试即可。

七、发布到 pub.dev(可选)

如果你的插件具有通用性,可以发布到 pub.dev 供社区使用。发布前需要准备:

  • 完善的 README.md、CHANGELOG.md、LICENSE;
  • 在 pubspec.yaml 中填写 homepage 和 version;
  • 执行 flutter pub publish --dry-run 检查是否有警告或错误。

正式发布时,由于国内网络环境,通常需要配置终端代理或使用镜像源。执行 flutter pub publish --server=https://pub.dartlang.org,然后根据提示完成 Google 账号授权即可。

写在最后

开发 Flutter 原生插件本质上是在做“桥梁工程”——Dart 端定义清晰的 API 接口,原生端忠实地执行平台特定的逻辑,MethodChannel 负责两端的消息传递。脚手架已经帮你处理了 90% 的样板代码,只需要在预留的“插槽”里填入业务逻辑。

建议从最简单的功能开始(比如返回一个系统版本号),把整个链路跑通之后,再逐步增加复杂度。当理解了数据是如何从 Dart 流到 Kotlin/Swift 再流回来的,剩下的就只是查对应平台的 API 文档而已了,当然,现在鸿蒙也再flutter中得到了支持,但是需要更换增强的fluttersdk,后面会介绍如何写鸿蒙原生插件。