Flutter艺术探索-Flutter插件开发:自定义Plugin实战指南

自己动手,开发一个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) {
相关推荐
向哆哆2 小时前
跨端开发实践:Flutter × OpenHarmony 构建垃圾回收分类知识区域
flutter·开源·鸿蒙·openharmony
kirk_wang2 小时前
Flutter艺术探索-EventChannel使用:原生事件流与Flutter交互
flutter·移动开发·flutter教程·移动开发教程
晚霞的不甘2 小时前
Flutter for OpenHarmony《智慧字典》中的沉浸式学习:成语测试与填空练习等功能详解
学习·flutter·ui·信息可视化·前端框架·鸿蒙
花卷HJ2 小时前
Flutter加载弹窗使用问题及解决方案
flutter
ujainu3 小时前
#Flutter + OpenHarmony高保真秒表 App 实现:主副表盘联动、计次记录与主题适配全解析
flutter
向哆哆3 小时前
Flutter × OpenHarmony 跨端实战:打造“智能垃圾分类助手”的快速分类入口模块
flutter·开源·鸿蒙·openharmony
时光慢煮3 小时前
构建跨端驾照学习助手:Flutter × OpenHarmony 的用户信息与驾照状态卡片实现
学习·flutter·开源·openharmony
向哆哆3 小时前
Flutter × OpenHarmony 跨端实战:垃圾分类应用顶部横幅组件的设计与实现
flutter·鸿蒙·openharmony·开源鸿蒙
微祎_3 小时前
Flutter for OpenHarmony:构建一个专业级 Flutter 番茄钟,深入解析状态机、定时器管理与专注力工具设计
开发语言·javascript·flutter