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.11.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.mdCHANGELOG.mdLICENSE
  9. 使用 flutter pub publish --dry-run 检查
  10. 使用 flutter pub publish 发布

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

相关推荐
我有满天星辰7 小时前
【Dart 语言学习教程 】 第二章:面向对象编程
学习·flutter·dart
●VON7 小时前
AtomGit Flutter鸿蒙客户端:API客户端与网络层
flutter·华为·架构·跨平台·harmonyos·鸿蒙
核电机组8 小时前
IOS原生APP集成Flutter
flutter·ios
唔668 小时前
在 Flutter 混合开发中,Android 原生层通知 Dart 界面更新状态
android·flutter
小书房8 小时前
移动开发跨平台方案之RN/Flutter/KMP/CMP
flutter·react native·react·跨平台·rn·kmp·cmp
●VON9 小时前
AtomGit Flutter鸿蒙客户端:安全JSON解析
安全·flutter·华为·json·harmonyos·鸿蒙
●VON9 小时前
AtomGit Flutter鸿蒙客户端:项目架构概览
flutter·华为·架构·harmonyos·鸿蒙
●VON10 小时前
AtomGit Flutter鸿蒙客户端:OAuth2认证与登录
flutter·华为·跨平台·harmonyos·鸿蒙
●VON10 小时前
AtomGit Flutter鸿蒙客户端:Tab导航架构
flutter·华为·架构·harmonyos·鸿蒙