自己动手,开发一个Flutter插件:从原理到上线的完整指南
写在前面:我们为什么需要自己开发插件?
用Flutter做跨平台开发,"写一次代码,两端都能用"的效率确实很吸引人。但当我们想调用摄像头、读取传感器数据、连接蓝牙或者访问本地文件时,就会发现Flutter框架本身并不能直接和手机硬件打交道。这时候,就需要Flutter插件来扮演"翻译官"的角色,在Dart代码和原生平台(Android/iOS)之间架起沟通的桥梁。
虽然pub.dev上已经有大量优秀的插件,但在实际项目,尤其是公司项目里,你总会遇到一些特殊需求:
- 公司内部有一套特定的SDK或硬件需要集成。
- 找到的现有插件功能不全,或者用起来不够顺手。
- 某个平台的性能有优化空间,需要自己动手"精调"。
- 想把一段复杂的业务逻辑打包成模块,方便团队复用。
今天,我们就通过创建一个增强版的设备信息插件,把Flutter插件开发的全过程走一遍。从设计思路、编写代码、性能调优到最终发布,每个环节都会讲到。
一、先弄明白:Flutter插件是怎么工作的?
1.1 核心:Platform Channel(平台通道)
Flutter插件之所以能跨平台,全靠Platform Channel这套异步消息传递机制。你可以把它想象成Dart和原生代码之间的一座桥,两边通过约定好的"暗号"进行通信。
简单来说,主要有三种"通道":
dart
// 三种Platform Channel的对比
┌────────────────┬──────────────────────┬─────────────────────────────┐
│ 通道类型 │ 是用来干什么的? │ 典型的使用场景 │
├────────────────┼──────────────────────┼─────────────────────────────┤
│ MethodChannel │ 方法调用和获取结果 │ 获取设备信息、调用原生功能 │
│ EventChannel │ 监听事件流(单向) │ 持续获取传感器、地理位置数据 │
│ BasicMessageChannel│ 传输基本消息 │ 传递自定义的数据结构 │
└────────────────┴──────────────────────┴─────────────────────────────┘
它们是如何协作的? 整个过程是异步的,不会卡住你的界面:
┌───────────┐ 发送请求 ┌───────────┐ 调用原生代码 ┌───────────┐
│ Flutter │ ------------------------------------> │ 平台通道 │ ------------------------------------> │ 原生平台 │
│ (Dart) │ <------------------------------------ │ (Channel) │ <------------------------------------ │(Java/Swift)│
│ │ 接收结果 │ │ 返回执行结果 │ │
└───────────┘ └───────────┘ └───────────┘
你需要了解的几个关键点:
- 全程异步:所有通信都不会阻塞UI线程,应用保持流畅。
- 类型安全:基础数据类型(如String, int, List, Map)可以自动转换。
- 线程友好:在原生端,回调方法会自动切换到主线程执行。
- 生命周期绑定:通道的生命周期通常和Widget绑定,管理起来很方便。
1.2 一个标准插件的"模样"
在动手写代码前,我们先看看一个规范的Flutter插件项目长什么样:
device_info_plus_custom/ # 你的插件根目录
├── lib/ # Dart层:这里写给其他开发者用的接口
│ ├── device_info_plus_custom.dart # 主类,对外暴露的API
│ └── device_info_plus_custom_method_channel.dart # 通道的具体实现(可选)
├── android/ # Android平台的原生代码
│ ├── src/main/kotlin/ # 你的Kotlin/Java代码放在这里
│ └── build.gradle # Android项目的依赖配置
├── ios/ # iOS平台的原生代码
│ ├── Classes/ # Swift/Objective-C代码放在这里
│ └── device_info_plus_custom.podspec # 给CocoaPods的配置文件
├── example/ # 【非常重要】一个完整的示例App
│ ├── lib/main.dart # 演示如何调用你的插件
│ └── pubspec.yaml # 示例项目自己的配置
├── test/ # 插件的单元测试
├── pubspec.yaml # 插件的"身份证",定义名称、版本、依赖
└── README.md # 项目的使用说明文档
二、动手实战:创建一个能监控电量的设备信息插件
接下来,我们真刀真枪地创建一个插件。这个插件不仅能获取常规的设备信息,还能监听电池状态的变化。
2.1 第一步:创建项目并完成基础配置
打开终端,用Flutter的命令行工具初始化项目:
bash
# 使用插件模板创建项目,指定支持Android和iOS平台
flutter create --template=plugin \
--platforms=android,ios \
--org=com.yourcompany \ # 改成你的组织名
--project-name=device_info_plus_custom \
--description="一个增强版设备信息插件,支持电池状态监控" \
device_info_plus_custom
# 进入项目目录
cd device_info_plus_custom
# 可以快速查看下生成的项目结构
tree -L 2
然后,编辑项目根目录的 pubspec.yaml 文件,这是插件的核心配置:
yaml
name: device_info_plus_custom
description: 一个用于获取详细设备信息(包括电池状态)的Flutter插件。
version: 1.0.0+1 # 版本号,+1代表构建号
homepage: https://github.com/yourusername/device_info_plus_custom # 你的项目主页
# 指定Flutter和Dart的版本要求
environment:
sdk: ">=3.0.0 <4.0.0"
flutter: ">=1.20.0"
dependencies:
flutter:
sdk: flutter
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^3.0.0 # 代码静态分析,让代码更规范
# Flutter插件的特定配置
flutter:
plugin:
platforms:
android:
package: com.yourcompany.device_info_plus_custom # Android包名
pluginClass: DeviceInfoPlusCustomPlugin # 插件主类名
ios:
pluginClass: DeviceInfoPlusCustomPlugin # iOS插件类名
2.2 第二步:编写Dart层的接口代码
这是插件对外的"门面",其他Flutter开发者调用的就是你在这里暴露的方法。
lib/device_info_plus_custom.dart:
dart
import 'dart:async';
import 'package:flutter/services.dart';
/// 设备信息的数据模型,用来结构化地保存信息
class DeviceInfoData {
final String deviceId;
final String deviceModel;
final String deviceBrand;
final String osVersion;
final String sdkVersion;
final double screenWidth;
final double screenHeight;
final double screenDensity;
final BatteryStatus batteryStatus;
final int? batteryLevel; // 电量百分比,0-100,null表示无法获取
final bool isPhysicalDevice;
final String? androidId; // Android设备特有的ID
final String? iosUuid; // iOS设备特有的UUID
// 构造函数(省略)
DeviceInfoData({...});
/// 将对象转换成Map,方便通过Channel传输
Map<String, dynamic> toMap() {
return {...};
}
/// 从Map还原出对象,用于接收Channel返回的数据
factory DeviceInfoData.fromMap(Map<String, dynamic> map) {
return DeviceInfoData(...);
}
@override
String toString() {
return '''
设备信息:
设备ID: $deviceId
型号: $deviceModel ($deviceBrand)
系统版本: $osVersion (SDK: $sdkVersion)
屏幕分辨率: ${screenWidth.toInt()}x${screenHeight.toInt()} @${screenDensity}x
电池状态: $batteryStatus${batteryLevel != null ? ' ($batteryLevel%)' : ''}
是否为真机: $isPhysicalDevice
${androidId != null ? ' Android ID: $androidId' : ''}
${iosUuid != null ? ' iOS UUID: $iosUuid' : ''}
''';
}
}
/// 电池状态枚举,与原生端保持一致
enum BatteryStatus {
unknown, // 未知
charging, // 充电中
discharging, // 放电中
full, // 已充满
notCharging, // 未充电
}
/// 插件的主类,所有功能都通过这个类提供
class DeviceInfoPlusCustom {
// 定义一个方法通道,用于一次性方法调用
static const MethodChannel _methodChannel = MethodChannel(
'com.yourcompany/device_info_plus_custom/methods', // 通道名称要唯一
);
// 定义一个事件通道,用于持续监听电池状态变化
static const EventChannel _batteryEventChannel = EventChannel(
'com.yourcompany/device_info_plus_custom/battery_events',
);
/// 获取完整的设备信息(核心方法)
static Future<DeviceInfoData> getDeviceInfo() async {
try {
// 通过通道调用原生方法 'getDeviceInfo'
final result = await _methodChannel.invokeMethod('getDeviceInfo');
// 将返回的Map数据转换成我们的数据模型
return DeviceInfoData.fromMap(Map<String, dynamic>.from(result as Map));
} on PlatformException catch (e) {
// 捕获平台调用异常(如权限不足、方法不存在)
throw DeviceInfoException(
'获取设备信息失败: ${e.message}',
code: e.code,
);
} catch (e) {
// 捕获其他未知异常
throw DeviceInfoException('发生未知错误: $e');
}
}
/// 获取一个电池状态变化的监听流
/// 使用示例:`DeviceInfoPlusCustom.batteryStatusStream.listen((status){...})`
static Stream<BatteryStatus> get batteryStatusStream {
return _batteryEventChannel
.receiveBroadcastStream() // 建立监听
.map((event) => BatteryStatus.values[event as int]) // 转换数据类型
.handleError((error) { // 处理监听过程中的错误
if (error is PlatformException) {
throw DeviceInfoException(
'监听电池事件失败: ${error.message}',
code: error.code,
);
}
throw error;
});
}
/// 检查当前设备是否支持电池监控
/// 在某些模拟器或特定设备上可能返回false
static Future<bool> isBatteryMonitoringSupported() async {
try {
return await _methodChannel.invokeMethod(
'isBatteryMonitoringSupported',
) as bool;
} on PlatformException {
// 如果调用方法本身出错,通常认为不支持
return false;
}
}
}
/// 自定义的异常类,方便调用者区分和处理错误
class DeviceInfoException implements Exception {
final String message;
final String? code;
DeviceInfoException(this.message, {this.code});
@override
String toString() => 'DeviceInfoException: $message${code != null ? ' (错误码: $code)' : ''}';
}
2.3 第三步:实现Android原生端(Kotlin)
Dart层已经准备好了"请求",现在需要在Android端编写"响应"的逻辑。
android/src/main/kotlin/com/yourcompany/device_info_plus_custom/DeviceInfoPlusCustomPlugin.kt:
kotlin
package com.yourcompany.device_info_plus_custom
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.BatteryManager
import android.os.Build
import android.provider.Settings
import androidx.annotation.NonNull
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.EventChannel
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
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class DeviceInfoPlusCustomPlugin : FlutterPlugin, MethodCallHandler {
private lateinit var methodChannel: MethodChannel
private lateinit var eventChannel: EventChannel
private lateinit var context: Context
private var batteryEventSink: EventChannel.EventSink? = null
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
// 保存应用上下文,后面会用到
context = flutterPluginBinding.applicationContext
// 初始化方法通道
methodChannel = MethodChannel(
flutterPluginBinding.binaryMessenger, // 消息信使
"com.yourcompany/device_info_plus_custom/methods" // 名称必须和Dart端一致
)
methodChannel.setMethodCallHandler(this) // 设置本类为处理方法调用的对象
// 初始化事件通道
eventChannel = EventChannel(
flutterPluginBinding.binaryMessenger,
"com.yourcompany/device_info_plus_custom/battery_events"
)
eventChannel.setStreamHandler(
object : EventChannel.StreamHandler {
// 当Flutter端开始监听时调用
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
batteryEventSink = events
registerBatteryReceiver() // 开始监听系统电池广播
}
// 当Flutter端取消监听时调用
override fun onCancel(arguments: Any?) {
batteryEventSink = null
unregisterBatteryReceiver() // 停止监听
}
}
)
}
// 处理从Dart端发过来的方法调用
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
when (call.method) {
"getDeviceInfo" -> {
// 获取设备信息可能涉及I/O,放在子线程中执行
CoroutineScope(Dispatchers.IO).launch {
try {
val deviceInfo = getDeviceInfo()
result.success(deviceInfo) // 成功则返回数据
} catch (e: Exception) {
result.error("GET_DEVICE_INFO_FAILED", e.message, null) // 失败返回错误
}
}
}
"isBatteryMonitoringSupported" -> result.success(true) // Android通常都支持
else -> result.notImplemented() // 调用未定义的方法
}
}
// 收集设备信息并组装成Map
private fun getDeviceInfo(): Map<String, Any> {
val displayMetrics = context.resources.displayMetrics
return mapOf(
"deviceId" to getDeviceId(),
"deviceModel" to Build.MODEL,
"deviceBrand" to Build.BRAND,
"osVersion" to Build.VERSION.RELEASE,
"sdkVersion" to Build.VERSION.SDK_INT.toString(),
"screenWidth" to displayMetrics.widthPixels,
"screenHeight" to displayMetrics.heightPixels,
"screenDensity" to displayMetrics.density,
"batteryStatus" to getBatteryStatus(),
"batteryLevel" to getBatteryLevel(),
"isPhysicalDevice" to !isEmulator(),
"androidId" to Settings.Secure.getString(
context.contentResolver,
Settings.Secure.ANDROID_ID
), // Android设备唯一标识
)
}
private fun getDeviceId(): String {
return try {
// 根据不同Android版本获取设备序列号
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Build.getSerial()
} else {
Build.SERIAL
} ?: "unknown"
} catch (e: SecurityException) {
// 如果没有权限,返回特定字符串
"permission_denied"
}
}
private fun getBatteryStatus(): Int {
// 通过广播接收器获取当前电池状态
val batteryStatus: Intent? = IntentFilter(Intent.ACTION_BATTERY_CHANGED).let { ifilter ->
context.registerReceiver(null, ifilter) // 注册一个一次性的接收器
}
// 将Android系统的状态码映射到我们定义的枚举索引
return when (batteryStatus?.getIntExtra(BatteryManager.EXTRA_STATUS, -1)) {
BatteryManager.BATTERY_STATUS_CHARGING -> 1 // 对应BatteryStatus.charging
BatteryManager.BATTERY_STATUS_DISCHARGING -> 2
BatteryManager.BATTERY_STATUS_FULL -> 3
BatteryManager.BATTERY_STATUS_NOT_CHARGING -> 4
else -> 0 // BatteryStatus.unknown
}
}
private fun getBatteryLevel(): Int? {
return try {
val batteryStatus: Intent? = IntentFilter(Intent.ACTION_BATTERY_CHANGED).let { ifilter ->
context.registerReceiver(null, ifilter)
}
val level = batteryStatus?.getIntExtra(BatteryManager.EXTRA_LEVEL, -1)
val scale = batteryStatus?.getIntExtra(BatteryManager.EXTRA_SCALE, -1)
if (level != -1 && scale != -1) {
(level * 100 / scale.toFloat()).toInt() // 计算百分比
} else {
null
}
} catch (e: Exception) {
null
}
}
// 判断当前是否运行在模拟器上
private fun isEmulator(): Boolean {
return (Build.FINGERPRINT.startsWith("generic")
|| Build.FINGERPRINT.startsWith("unknown")
|| Build.MODEL.contains("google_sdk")
|| Build.MODEL.contains("Emulator")
|| Build.MODEL.contains("Android SDK built for x86")
|| Build.MANUFACTURER.contains("Genymotion")
|| (Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic"))
|| "google_sdk" == Build.PRODUCT)
}
private fun registerBatteryReceiver() {
// 实际开发中,这里应注册一个真正的BroadcastReceiver
// 来监听ACTION_BATTERY_CHANGED广播,并通过eventSink发送给Flutter端
// 此处为简化示例,略去具体实现
}
private fun unregisterBatteryReceiver() {
// 取消注册广播接收器
}
override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
// 插件卸载时,清理资源
methodChannel.setMethodCallHandler(null)
eventChannel.setStreamHandler(null)
}
}
2.4 第四步:实现iOS原生端(Swift)
iOS端的逻辑与Android类似,但使用的是苹果的API。
ios/Classes/DeviceInfoPlusCustomPlugin.swift:
swift
import Flutter
import UIKit
public class DeviceInfoPlusCustomPlugin: NSObject, FlutterPlugin {
private var eventSink: FlutterEventSink?
public static func register(with registrar: FlutterPluginRegistrar) {
// 注册方法通道
let methodChannel = FlutterMethodChannel(
name: "com.yourcompany/device_info_plus_custom/methods",
binaryMessenger: registrar.messenger()
)
// 注册事件通道
let eventChannel = FlutterEventChannel(
name: "com.yourcompany/device_info_plus_custom/battery_events",
binaryMessenger: registrar.messenger()
)
let instance = DeviceInfoPlusCustomPlugin()
registrar.addMethodCallDelegate(instance, channel: methodChannel)
eventChannel.setStreamHandler(instance) // 设置事件流处理器
}
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
switch call.method {
case "getDeviceInfo":
getDeviceInfo(result: result)
case "isBatteryMonitoringSupported":
result(true) // iOS设备通常支持
default:
result(FlutterMethodNotImplemented)
}
}
private func getDeviceInfo(result: @escaping FlutterResult) {
let device = UIDevice.current
let screen = UIScreen.main
var deviceInfo: [String: Any] = [
"deviceId": getDeviceIdentifier(),
"deviceModel": device.model,
"deviceBrand": "Apple",
"osVersion": device.systemVersion,
"sdkVersion": UIDevice.current.systemVersion,
"screenWidth": screen.bounds.width * screen.scale, // 物理像素
"screenHeight": screen.bounds.height * screen.scale,
"screenDensity": Double(screen.scale), // 屏幕缩放因子
"batteryStatus": getBatteryStatus().rawValue,
"isPhysicalDevice": !isSimulator(),
"iosUuid": getDeviceIdentifier(),
]
// 尝试获取电池电量
if let batteryLevel = getBatteryLevel() {
deviceInfo["batteryLevel"] = batteryLevel
}
result(deviceInfo)
}
private func getDeviceIdentifier() -> String {
// 使用厂商标识符,在应用重装后保持不变(除非系统重置)
return UIDevice.current.identifierForVendor?.uuidString ?? "unknown"
}
private func getBatteryStatus() -> BatteryStatus {
let device = UIDevice.current
// 确保电池监控是开启状态
if !device.isBatteryMonitoringEnabled {
device.isBatteryMonitoringEnabled = true
}
switch device.batteryState {
case .charging:
return .charging
case .full:
return .full
case .unplugged:
return .discharging
case .unknown:
return .unknown
@unknown default:
return .unknown
}
}
private func getBatteryLevel() -> Int? {
let device = UIDevice.current
if device.isBatteryMonitoringEnabled {
let level = Int(device.batteryLevel * 100) // batteryLevel范围是0.0到1.0
return level >= 0 ? level : nil // 电池未监控时返回-1
}
return nil
}
private func isSimulator() -> Bool {
#if targetEnvironment(simulator)
return true
#else
return false
#endif
}
}
// 扩展:实现事件通道的流处理器协议
extension DeviceInfoPlusCustomPlugin: FlutterStreamHandler {
public func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
self.eventSink = events
startBatteryMonitoring()
return nil
}
public func onCancel(withArguments arguments: Any?) -> FlutterError? {
eventSink = nil
stopBatteryMonitoring()
return nil
}
private func startBatteryMonitoring() {
UIDevice.current.isBatteryMonitoringEnabled = true
// 监听系统发出的电池状态变化通知
NotificationCenter.default.addObserver(
self,
selector: #selector(batteryStateDidChange),
name: UIDevice.batteryStateDidChangeNotification,
object: nil
)
// 如果需要监听电量百分比变化,也可以添加这个通知
NotificationCenter.default.addObserver(
self,
selector: #selector(batteryLevelDidChange),
name: UIDevice.batteryLevelDidChangeNotification,
object: nil
)
}
private func stopBatteryMonitoring() {
// 移除监听并关闭监控以节省资源
NotificationCenter.default.removeObserver(self)
UIDevice.current.isBatteryMonitoringEnabled = false
}
@objc private func batteryStateDidChange() {
// 当电池状态变化时,通过事件通道发送新的状态给Flutter端
let status = getBatteryStatus()
eventSink?(status.rawValue)
}
@objc private func batteryLevelDidChange() {
// 可以在这里发送电量百分比变化事件
// 例如:eventSink?(["level": getBatteryLevel()])
}
}
// 定义电池状态枚举,其原始值(rawValue)与Dart端的枚举索引保持一致
enum BatteryStatus: Int {
case unknown = 0
case charging = 1
case discharging = 2
case full = 3
case notCharging = 4
}
三、编写示例应用,验证插件功能
一个优秀的插件必须附带一个清晰的示例。我们在example/目录下创建一个简单的演示App。
example/lib/main.dart:
dart
import 'package:device_info_plus_custom/device_info_plus_custom.dart';
import 'package:flutter/material.dart';
void main() => runApp(DeviceInfoApp());
class DeviceInfoApp extends StatelessWidget {
@override
Widget build(BuildContext context) {