手机检测准确率88.8%!DAMO-YOLO WebUI实测体验

核心内容摘要

桥梁裂纹“无处遁形”:DIC技术精准捕捉铁路箱梁弯曲破坏全过程
设计师必备:用Meixiong Niannian批量生成商业海报

Lucide React 详解

自己动手开发一个Flutter插件从原理到上线的完整指南写在前面我们为什么需要自己开发插件用Flutter做跨平台开发“写一次代码两端都能用”的效率确实很吸引人。

但当我们想调用摄像头、读取传感器数据、连接蓝牙或者访问本地文件时就会发现Flutter框架本身并不能直接和手机硬件打交道。

这时候就需要Flutter插件来扮演“翻译官”的角色在Dart代码和原生平台Android/iOS之间架起沟通的桥梁。

虽然pub.dev上已经有大量优秀的插件但在实际项目尤其是公司项目里你总会遇到一些特殊需求公司内部有一套特定的SDK或硬件需要集成。

找到的现有插件功能不全或者用起来不够顺手。

某个平台的性能有优化空间需要自己动手“精调”。

想把一段复杂的业务逻辑打包成模块方便团队复用。

今天我们就通过创建一个增强版的设备信息插件把Flutter插件开发的全过程走一遍。

从设计思路、编写代码、性能调优到最终发布每个环节都会讲到。

先弄明白Flutter插件是怎么工作的

1 核心Platform Channel平台通道Flutter插件之所以能跨平台全靠Platform Channel这套异步消息传递机制。

你可以把它想象成Dart和原生代码之间的一座桥两边通过约定好的“暗号”进行通信。

简单来说主要有三种“通道”// 三种Platform Channel的对比 ┌────────────────┬──────────────────────┬─────────────────────────────┐ │ 通道类型 │ 是用来干什么的 │ 典型的使用场景 │ ├────────────────┼──────────────────────┼─────────────────────────────┤ │ MethodChannel │ 方法调用和获取结果 │ 获取设备信息、调用原生功能 │ │ EventChannel │ 监听事件流单向 │ 持续获取传感器、地理位置数据 │ │ BasicMessageChannel│ 传输基本消息 │ 传递自定义的数据结构 │ └────────────────┴──────────────────────┴─────────────────────────────┘它们是如何协作的整个过程是异步的不会卡住你的界面┌───────────┐ 发送请求 ┌───────────┐ 调用原生代码 ┌───────────┐ │ Flutter │ ———————————— │ 平台通道 │ ———————————— │ 原生平台 │ │ (Dart) │ ———————————— │ (Channel) │ ———————————— │(Java/Swift)│ │ │ 接收结果 │ │ 返回执行结果 │ │ └───────────┘ └───────────┘ └───────────┘你需要了解的几个关键点全程异步所有通信都不会阻塞UI线程应用保持流畅。

类型安全基础数据类型如String, int, List, Map可以自动转换。

线程友好在原生端回调方法会自动切换到主线程执行。

生命周期绑定通道的生命周期通常和Widget绑定管理起来很方便。

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 # 项目的使用说明文档

动手实战创建一个能监控电量的设备信息插件接下来我们真刀真枪地创建一个插件。

这个插件不仅能获取常规的设备信息还能监听电池状态的变化。

1 第一步创建项目并完成基础配置打开终端用Flutter的命令行工具初始化项目# 使用插件模板创建项目指定支持Android和iOS平台 flutter create --templateplugin \ --platformsandroid,ios \ --orgcom.yourcompany \ # 改成你的组织名 --project-namedevice_info_plus_custom \ --description一个增强版设备信息插件支持电池状态监控 \ device_info_plus_custom # 进入项目目录 cd device_info_plus_custom # 可以快速查看下生成的项目结构 tree -L 2然后编辑项目根目录的pubspec.yaml文件这是插件的核心配置name: device_info_plus_custom description: 一个用于获取详细设备信息包括电池状态的Flutter插件。

version:

1.

01 # 版本号1代表构建号 homepage: https://github.com/yourusername/device_info_plus_custom # 你的项目主页 # 指定Flutter和Dart的版本要求 environment: sdk:

3.

0

4.

0 flutter:

1.

2

0 dependencies: flutter: sdk: flutter dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^

3.

0 # 代码静态分析让代码更规范 # Flutter插件的特定配置 flutter: plugin: platforms: android: package: com.yourcompany.device_info_plus_custom # Android包名 pluginClass: DeviceInfoPlusCustomPlugin # 插件主类名 ios: pluginClass: DeviceInfoPlusCustomPlugin # iOS插件类名

2 第二步编写Dart层的接口代码这是插件对外的“门面”其他Flutter开发者调用的就是你在这里暴露的方法。

lib/device_info_plus_custom.dartimport 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; // 电量百分比

null表示无法获取 final bool isPhysicalDevice; final String? androidId; // Android设备特有的ID final String? iosUuid; // iOS设备特有的UUID // 构造函数省略 DeviceInfoData({...}); /// 将对象转换成Map方便通过Channel传输 MapString, dynamic toMap() { return {...}; } /// 从Map还原出对象用于接收Channel返回的数据 factory DeviceInfoData.fromMap(MapString, 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 FutureDeviceInfoData getDeviceInfo() async { try { // 通过通道调用原生方法 ‘getDeviceInfo’ final result await _methodChannel.invokeMethod(getDeviceInfo); // 将返回的Map数据转换成我们的数据模型 return DeviceInfoData.fromMap(MapString, 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 StreamBatteryStatus 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 Futurebool 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) : }; }

3 第三步实现Android原生端KotlinDart层已经准备好了“请求”现在需要在Android端编写“响应”的逻辑。

android/src/main/kotlin/com/yourcompany/device_info_plus_custom/DeviceInfoPlusCustomPlugin.ktpackage 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(): MapString, 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, -

) { 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, -

val scale batteryStatus?.getIntExtra(BatteryManager.EXTRA_SCALE, -

if (level ! -1 scale ! -

{ (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 x

|| 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) } }

4 第四步实现iOS原生端SwiftiOS端的逻辑与Android类似但使用的是苹果的API。

ios/Classes/DeviceInfoPlusCustomPlugin.swiftimport 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 *

// batteryLevel范围是

0到

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.dartimport 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) {

9·1行情网站-9·1行情网站应用

百度百家号客服电话人工服务

123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123