Flutter Plugin 开发教程:从零创建原生插件到发布 pub.dev 完整流程
本文面向 Flutter 初学者,演示如何创建一个 Flutter Plugin,并通过 Android / iOS 原生代码向 Dart 层返回数据。最后也会说明如何进行发布前检查,以及如何发布到 pub.dev。
本文示例插件名为 native_location,它通过 MethodChannel 从原生平台返回一组经纬度数据。为了方便理解,示例中返回的是固定坐标;真实项目中可以继续接入 Android Location API 或 iOS Core Location。
参考官方文档:
- Flutter 插件开发:https://docs.flutter.dev/packages-and-plugins/developing-packages
- Dart 包发布:https://dart.dev/tools/pub/publishing
- pubspec 配置:https://dart.dev/tools/pub/pubspec
- Android 定位权限:https://developer.android.com/develop/sensors-and-location/location/permissions
- Apple Core Location 授权:https://developer.apple.com/documentation/corelocation/requesting-authorization-to-use-location-services
一、Plugin 是什么?
Flutter 中常见的代码复用方式是 package。普通 Dart package 通常只包含 Dart 代码,而 Flutter Plugin 是一种更特殊的 package,它可以同时包含:
- Dart API
- Android 原生实现,通常是 Kotlin 或 Java
- iOS 原生实现,通常是 Swift 或 Objective-C
example示例项目
当 Flutter 需要调用系统能力时,就经常会用到 Plugin,例如定位、相机、蓝牙、传感器、剪贴板等。
简单理解:
text
Flutter 页面
↓
Dart 插件 API
↓
MethodChannel
↓
Android / iOS 原生代码
二、创建插件项目
执行命令:
bash
flutter create --template=plugin --platforms=android,ios native_location
进入项目目录:
bash
cd native_location
创建完成后,常见目录结构如下:
text
native_location/
lib/
native_location.dart
native_location_method_channel.dart
native_location_platform_interface.dart
android/
ios/
example/
pubspec.yaml
README.md
CHANGELOG.md
LICENSE
几个核心目录的作用:
lib/:插件暴露给 Flutter 使用者的 Dart APIandroid/:Android 平台原生实现ios/:iOS 平台原生实现example/:插件示例 App,用于调试和演示插件用法pubspec.yaml:包名、版本、依赖、平台配置等元信息
三、Dart 层对外 API
插件使用者一般不会直接调用 MethodChannel,而是调用插件暴露出来的 Dart 类。
例如在 lib/native_location.dart 中定义:
dart
import 'native_location_platform_interface.dart';
class NativeLocation {
Future<String?> getPlatformVersion() {
return NativeLocationPlatform.instance.getPlatformVersion();
}
Future<Map<String, dynamic>> getLocation() {
return NativeLocationPlatform.instance.getLocation();
}
}
这里定义了两个方法:
getPlatformVersion():获取平台版本,用于验证插件通信是否正常getLocation():获取位置信息,这是本文示例新增的方法
四、Platform Interface 的作用
Flutter 插件模板中通常会有一个 native_location_platform_interface.dart 文件。它的作用是定义平台接口,方便未来扩展到 Android、iOS、Web、macOS、Windows 等不同平台。
示例:
dart
abstract class NativeLocationPlatform extends PlatformInterface {
NativeLocationPlatform() : super(token: _token);
static final Object _token = Object();
static NativeLocationPlatform _instance = MethodChannelNativeLocation();
static NativeLocationPlatform get instance => _instance;
static set instance(NativeLocationPlatform instance) {
PlatformInterface.verifyToken(instance, _token);
_instance = instance;
}
Future<String?> getPlatformVersion() {
throw UnimplementedError('platformVersion() has not been implemented.');
}
Future<Map<String, dynamic>> getLocation() {
throw UnimplementedError('getLocation() has not been implemented.');
}
}
这里有一个容易让初学者疑惑的点:为什么要有 PlatformInterface.verifyToken(instance, _token)?
这是 plugin_platform_interface 推荐的写法,用来避免外部随意使用 implements 破坏平台接口约定。真实的平台实现应该继承这个接口;测试代码如果需要 mock,可以使用 MockPlatformInterfaceMixin。
五、使用 MethodChannel 调用原生代码
native_location_method_channel.dart 负责真正发起平台通道调用。
示例:
dart
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'native_location_platform_interface.dart';
class MethodChannelNativeLocation extends NativeLocationPlatform {
@visibleForTesting
final methodChannel = const MethodChannel('native_location');
@override
Future<String?> getPlatformVersion() async {
final version = await methodChannel.invokeMethod<String>('getPlatformVersion');
return version;
}
@override
Future<Map<String, dynamic>> getLocation() async {
final location =
await methodChannel.invokeMethod<Map<String, dynamic>>('getLocation');
return location ?? {};
}
}
重点看两个地方:
MethodChannel('native_location'):通道名称,Android 和 iOS 要使用同一个名称invokeMethod('getLocation'):调用原生端的方法名,原生端也要处理同名方法
六、Android 原生实现
Android 插件代码通常在:
text
android/src/main/kotlin/com/example/native_location/NativeLocationPlugin.kt
示例代码:
kotlin
package com.example.native_location
import 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
class NativeLocationPlugin :
FlutterPlugin,
MethodCallHandler {
private lateinit var channel: MethodChannel
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "native_location")
channel.setMethodCallHandler(this)
}
override fun onMethodCall(
call: MethodCall,
result: Result
) {
if (call.method == "getPlatformVersion") {
result.success("Android ${android.os.Build.VERSION.RELEASE}")
} else if (call.method == "getLocation") {
result.success(
mapOf(
"latitude" to 37.774929,
"longitude" to -122.419416,
)
)
} else {
result.notImplemented()
}
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
channel.setMethodCallHandler(null)
}
}
这段代码做了三件事:
- 创建和 Dart 端同名的
MethodChannel - 监听 Dart 端调用的方法
- 当方法名是
getLocation时,返回一个包含经纬度的Map
本文为了演示通信流程,直接返回固定经纬度。如果要做真实定位,需要接入 Android 定位能力。
Android 定位功能通常需要在 AndroidManifest.xml 中声明权限,例如:
xml
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
如果需要后台定位,还会涉及 ACCESS_BACKGROUND_LOCATION,并且 Google Play 对后台定位有额外政策要求。初学者建议先从前台定位开始。
七、iOS 原生实现
iOS 插件代码通常在:
text
ios/Classes/NativeLocationPlugin.swift
示例代码:
swift
import Flutter
import UIKit
public class NativeLocationPlugin: NSObject, FlutterPlugin {
public static func register(with registrar: FlutterPluginRegistrar) {
let channel = FlutterMethodChannel(name: "native_location", binaryMessenger: registrar.messenger())
let instance = NativeLocationPlugin()
registrar.addMethodCallDelegate(instance, channel: channel)
}
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
switch call.method {
case "getPlatformVersion":
result("iOS " + UIDevice.current.systemVersion)
case "getLocation":
result([
"latitude": 37.774929,
"longitude": -122.419416,
])
default:
result(FlutterMethodNotImplemented)
}
}
}
iOS 端同样要保证两点:
FlutterMethodChannel的名称和 Dart 端一致call.method的方法名和 Dart 调用的方法名一致
真实定位功能需要使用 Apple 的 CoreLocation。根据 Apple 官方文档,定位属于敏感信息,App 必须先获取用户授权。
如果 App 需要使用前台定位,通常要在 App 的 Info.plist 中配置:
xml
<key>NSLocationWhenInUseUsageDescription</key>
<string>需要获取当前位置用于展示定位信息</string>
如果缺少必要的权限说明,系统授权请求会失败。
八、在 example 中使用插件
进入 example 目录:
bash
cd example
flutter pub get
flutter run
在 example App 中可以这样调用:
dart
import 'package:native_location/native_location.dart';
final plugin = NativeLocation();
final location = await plugin.getLocation();
print(location['latitude']);
print(location['longitude']);
如果要在页面上展示,可以把返回值放到 setState 中更新 UI。
例如:
dart
Future<void> loadLocation() async {
final location = await plugin.getLocation();
setState(() {
latitude = location['latitude'].toString();
longitude = location['longitude'].toString();
});
}
九、编写和运行测试
回到插件根目录:
bash
cd ..
运行测试:
bash
flutter test
运行代码分析:
bash
flutter analyze
如果有 example 测试,也可以进入 example 执行:
bash
cd example
flutter test
测试 mock 平台接口时,可以使用 MockPlatformInterfaceMixin:
dart
class MockNativeLocationPlatform
with MockPlatformInterfaceMixin
implements NativeLocationPlatform {
@override
Future<String?> getPlatformVersion() => Future.value('42');
@override
Future<Map<String, dynamic>> getLocation() {
return Future.value({
'latitude': 37.774929,
'longitude': -122.419416,
});
}
}
这样可以在不真正调用 Android / iOS 原生代码的情况下测试 Dart API。
十、发布前检查 pubspec.yaml
发布到 pub.dev 前,pubspec.yaml 很重要。它会影响 pub.dev 页面展示和用户安装。
示例:
yaml
name: native_location
description: A simple Flutter plugin example for accessing native location data.
version: 0.0.1
repository: https://github.com/your-name/native_location
environment:
sdk: ^3.0.0
flutter: ">=3.3.0"
dependencies:
flutter:
sdk: flutter
plugin_platform_interface: ^2.0.2
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^6.0.0
需要注意:
description建议使用英文,长度适中,清楚说明包的作用version要遵循语义化版本,例如0.0.1、1.0.0repository建议填写真实源码仓库地址environment.sdk不要随意设置得太高,否则会限制用户使用
十一、补充 README、CHANGELOG、LICENSE
发布前建议重点检查这几个文件。
README.md
README 建议包含:
- 插件介绍
- 安装方式
- Android 配置
- iOS 配置
- 使用示例
- 注意事项
CHANGELOG.md
示例:
markdown
## 0.0.1
- Initial release.
- Add Android and iOS MethodChannel examples.
- Add getLocation API example.
LICENSE
Dart 官方发布文档要求发布包包含 LICENSE 文件。可以根据项目情况选择 MIT、BSD-3-Clause、Apache-2.0 等许可证。
不要保留模板里的:
text
TODO: Add your license here.
十二、检查将要发布哪些文件
根据 Dart 官方文档,发布包默认会包含包根目录下的文件,但会排除:
- 隐藏文件或隐藏目录,例如
.git/、.dart_tool/ .gitignore或.pubignore忽略的文件
也就是说,lib/ 会发布,example/ 通常也会发布。example/ 对用户很有帮助,因为它展示了插件怎么使用。
但是构建产物不应该发布,例如:
text
example/build/
example/.dart_tool/
build/
.dart_tool/
最终以 dry-run 输出的文件列表为准。
十三、发布前 dry-run 检查
正式发布前,先运行:
bash
flutter pub publish --dry-run
这个命令不会真正发布,只会检查包是否符合 pub.dev 的发布规则,并显示将要上传的文件列表。
Flutter 插件建议使用:
bash
flutter pub publish --dry-run
普通 Dart 包也可以使用:
bash
dart pub publish --dry-run
十四、正式发布到 pub.dev
确认 dry-run 没有严重问题后,执行:
bash
flutter pub publish
发布时需要登录 pub.dev 使用的 Google 账号。
官方文档提醒:发布是长期有效的。一个版本发布后不能直接覆盖,只能发布新的版本。因此发布前一定要确认:
- 版本号正确
README.md内容完整CHANGELOG.md已更新LICENSE已填写pubspec.yaml元信息正确- 没有上传无关文件
- example 可以正常运行
- 测试通过
- dry-run 检查通过
十五、常见问题
1. publish --dry-run 报 command not found
原因是命令写错了。
错误命令:
bash
publish --dry-run
正确命令:
bash
flutter pub publish --dry-run
2. flutter publish --dry-run 报找不到 publish
原因是 publish 不是 flutter 的一级命令。
错误命令:
bash
flutter publish --dry-run
正确命令:
bash
flutter pub publish --dry-run
3. 发布后还能修改同一个版本吗?
不能。根据 Dart 官方发布文档,一个版本发布后不能直接覆盖。如果需要修改代码或文档,需要提升版本号后重新发布。
例如:
yaml
version: 0.0.2
然后再次执行:
bash
flutter pub publish
4. lib 目录会一起发布吗?
会。lib/ 是 Dart / Flutter 包最核心的源码目录,用户依赖插件后,实际使用的就是这里暴露出来的 API。
5. example 目录会一起发布吗?
通常会。example/ 可以帮助用户理解插件怎么使用。只要确认不要把 example/build/、example/.dart_tool/ 等构建产物发布上去即可。
十六、总结
构建一个 Flutter Plugin 的核心流程是:
- 使用
flutter create --template=plugin创建插件 - 在 Dart 层定义插件 API
- 使用
MethodChannel调用原生方法 - Android / iOS 分别实现对应方法
- 在 example 中验证插件功能
- 编写测试并运行
flutter test - 运行
flutter analyze - 补充
README.md、CHANGELOG.md、LICENSE - 使用
flutter pub publish --dry-run检查 - 使用
flutter pub publish发布
对于初学者来说,可以先从返回固定数据开始理解通信流程。等熟悉 Flutter 和原生平台之间的调用方式后,再逐步接入真实的 Android 和 iOS 系统 API。