Flutter Plugin 开发教程:从零创建原生插件到发布 pub.dev 完整流程

Flutter Plugin 开发教程:从零创建原生插件到发布 pub.dev 完整流程

本文面向 Flutter 初学者,演示如何创建一个 Flutter Plugin,并通过 Android / iOS 原生代码向 Dart 层返回数据。最后也会说明如何进行发布前检查,以及如何发布到 pub.dev。

本文示例插件名为 native_location,它通过 MethodChannel 从原生平台返回一组经纬度数据。为了方便理解,示例中返回的是固定坐标;真实项目中可以继续接入 Android Location API 或 iOS Core Location。

参考官方文档:

一、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 API
  • android/: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.0
  • repository 建议填写真实源码仓库地址
  • 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 的核心流程是:

  1. 使用 flutter create --template=plugin 创建插件
  2. 在 Dart 层定义插件 API
  3. 使用 MethodChannel 调用原生方法
  4. Android / iOS 分别实现对应方法
  5. 在 example 中验证插件功能
  6. 编写测试并运行 flutter test
  7. 运行 flutter analyze
  8. 补充 README.md、CHANGELOG.md、LICENSE
  9. 使用 flutter pub publish --dry-run 检查
  10. 使用 flutter pub publish 发布

对于初学者来说,可以先从返回固定数据开始理解通信流程。等熟悉 Flutter 和原生平台之间的调用方式后,再逐步接入真实的 Android 和 iOS 系统 API。

相关推荐
yuanlaile6 小时前
2026年全新Flutter 跨平台开发全栈学习资料整理
flutter·getx·鸿蒙 next·flutter开发鸿蒙·flutter入门实战·flutter全栈
恋猫de小郭17 小时前
Genkit Dart 1.0 发布,Flutter 原生的 AI Agent 终于完整了
android·前端·flutter
第10086个小白17 小时前
flutter桌面端打包方式(Inno Setup Compiler)版本
前端·flutter
陆断枫1 天前
Flutter 3.47 动画引擎盘点:Impeller 全平台,和那些"新特性"
flutter
程序员老刘1 天前
Flet 1.0 思路很好,可惜来晚了
flutter·ai编程·客户端
m0_738185822 天前
Flutter 鸿蒙化实战:qr_code_scanner_plus 适配 OpenHarmony,二维码扫描
flutter·华为·harmonyos·鸿蒙
恋猫de小郭2 天前
Flutter + EmbeddingGemma 2,谷歌发布完全端侧的 AI Edge Foresight
android·前端·flutter
君赏2 天前
基于 FlutterPatch 快速接入 Flutter 热更新
flutter
律宏阔2 天前
Dart FFI 内存管理:用 using + Arena 替代嵌套 try-finally
前端·flutter
律宏阔2 天前
Flutter 调用 Go:从 c-shared + ffigen 到 @Native + Native Assets 踩坑记录
前端·flutter