十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Flutter 三方库适配 OpenHarmony:基于 drag_and_drop 与 UDMF 实现文本、文件和目录拖放

Flutter 三方库适配 OpenHarmony:基于 drag_and_drop 与 UDMF 实现文本、文件和目录拖放 Flutter 三方库适配 OpenHarmony基于 drag_and_drop 与 UDMF 实现文本、文件和目录拖放前言Flutter 的drag_and_drop_flutter已经抽象了文本、文件和目录拖放模型但原仓库没有 OpenHarmony 平台实现。本次适配基于 OpenHarmony Flutter 引擎内置的drag_and_drop框架结合 UDMF 完成跨应用拖放数据接收。本文记录完整适配过程包括权限判断、联邦插件结构、ArkTS 原生实现、Flutter 通道协议、文件 URI 处理、目录拖放、安全校验和验证方式。效果如下图所示。说明本文代码以当前仓库的实现为准。不同 OpenHarmony SDK 对 UDMF 记录字段的具体类型可能存在差异最终应以目标 DevEco 工程的 SDK 定义为准。一、适配目标1.1 原插件能力原插件通过平台接口暴露DragDropArea业务层可以注册以下回调DragDropArea(onDragEnter:(items){},onDragExit:(){},onDrop:(data){},child:constText(Drop here),)平台接口中的DataTransferItem支持两类数据DataTransferItem.data字符串数据例如text/plainDataTransferItem.file文件或目录引用。1.2 OpenHarmony 目标本次适配实现使用 Flutter OpenHarmony 引擎的drag_and_drop回调体系使用 UDMF 解析拖放记录支持文本、文件和目录 URI通过 EventChannel 将拖放事件发送到 Dart保持原有DragDropAreaAPI 不变提供完整的 OpenHarmony HAR 工程和示例宿主工程。二、三方应用权限确认2.1 是否需要显示模式权限普通三方应用接收 UDMF 拖放不需要申请“设置显示模式”权限。显示模式、窗口模式和拖放能力属于不同系统能力能力用途是否是拖放前置权限窗口显示模式全屏、分屏、悬浮窗否drag_and_drop接收拖入和释放事件是实现能力不是显示权限UDMF描述和传递数据否URI 访问授权读取外部文件或目录文件场景需要关注因此本插件的module.json5不增加显示模式权限。示例应用只保留 Flutter 应用运行所需的网络权限{ module: { requestPermissions: [ { name: ohos.permission.INTERNET } ] } }2.2 文件和目录的授权文本拖放一般可以直接读取。文件和目录拖放通常携带 URI目标应用是否能读取内容取决于源端是否授予访问能力。推荐的数据链路如下拖放事件 ↓ UDMF 记录 ↓ 文件/目录 URI ↓ 检查 URI 访问能力 ↓ 按需读取或复制到应用沙箱 ↓ Flutter 业务层处理不要把 URI 直接替换为绝对路径也不要假设外部 URI 一定属于当前应用沙箱。三、仓库结构3.1 联邦插件结构本仓库新增drag_and_drop_flutter_ohospackages/ ├── drag_and_drop_flutter/ ├── drag_and_drop_flutter_platform_interface/ ├── drag_and_drop_flutter_web/ └── drag_and_drop_flutter_ohos/ ├── lib/ │ └── drag_and_drop_flutter_ohos.dart ├── ohos/ │ ├── AppScope/ │ ├── src/main/module.json5 │ ├── src/main/ets/components/plugin/ │ │ └── DragAndDropFlutterOhosPlugin.ets │ ├── build-profile.json5 │ ├── hvigorfile.ts │ ├── oh-package.json5 │ └── index.ets ├── pubspec.yaml └── README.md3.2 示例应用结构Flutter 示例也生成了完整 OpenHarmony 宿主packages/drag_and_drop_flutter/example/ohos/ ├── AppScope/ ├── entry/ │ ├── src/main/ets/entryability/EntryAbility.ets │ ├── src/main/ets/pages/Index.ets │ ├── src/main/ets/plugins/GeneratedPluginRegistrant.ets │ └── src/main/module.json5 ├── build-profile.json5 ├── hvigorfile.ts └── oh-package.json5四、使用 drag_and_drop 框架4.1 Flutter 引擎回调OpenHarmony Flutter 引擎的FlutterPage已经通过 ArkUI 的拖放修饰器监听.onDragEnter((event:DragEvent,extraParams:string){FlutterManager.getInstance().getDragEnterCbs().forEach(callback{callback.do(event,extraParams);});}).onDragMove((event:DragEvent,extraParams:string){FlutterManager.getInstance().getDragMoveCbs().forEach(callback{callback.do(event,extraParams);});}).onDragLeave((event:DragEvent,extraParams:string){FlutterManager.getInstance().getDragLeaveCbs().forEach(callback{callback.do(event,extraParams);});}).onDrop((event:DragEvent,extraParams:string){FlutterManager.getInstance().getDropCbs().forEach(callback{callback.do(event,extraParams);});})插件不再创建自定义悬浮层也不需要额外的原生 PlatformView。插件只需向FlutterManager注册回调。4.2 回调注册constmanagerFlutterManager.getInstance();constenterIdmanager.addDragEnterCb({do:(_event:DragEvent,extra:string):void{emitDrop(enter,recordsFrom(extra));}});constdropIdmanager.addDropCb({do:(event:DragEvent,extra:string):void{emitDrop(drop,recordsFromEvent(event,extra));}});组件销毁或停止监听时需要移除回调manager.removeDragEnterCb(enterId);manager.removeDropCb(dropId);五、UDMF 数据归一化5.1 统一数据模型原插件的数据模型不要求 Flutter 了解 UDMF 具体类型因此在 ArkTS 层做一次归一化typeDropItemRecordstring,Object;exportclassUdmfDropNormalizer{staticnormalize(records:Arrayany):DropItem[]{returnrecords.map((record:any):DropItem{consttypeString(record.dataType??record.mimeType??record.type??);consturiString(record.uri??record.fileUri??);if(typetext/plain||typetext){return{type:text/plain,data:String(record.data??record.text??),isFile:false,};}if(uri.length0){return{type:record.isDirectorytrue?inode/directory:type||application/octet-stream,uri,name:String(record.name??),isFile:true,};}return{type:type||application/octet-stream,data:,isFile:false,};});}}5.2 文本记录文本记录转换为DataTransferItem.data{type:text/plain,data:来自其他应用的文本,isFile:false}5.3 文件记录文件记录转换为文件引用{type:application/pdf,uri:file://example/report.pdf,name:report.pdf,isFile:true}5.4 目录记录目录仍然使用文件引用接口承载但类型设为inode/directory{type:inode/directory,uri:file://example/project,name:project,isFile:true}Flutter 层可以根据类型决定是读取单文件还是递归枚举目录。六、ArkTS 插件实现6.1 插件声明exportdefaultclassDragAndDropFlutterOhosPluginimplementsFlutterPlugin,MethodCallHandler,StreamHandler{privatemethodChannel:MethodChannel|nullnull;privateeventChannel:EventChannel|nullnull;privatesink:EventSink|nullnull;privatecallbackIds:number[][];}6.2 通道注册onAttachedToEngine(binding:FlutterPluginBinding):void{this.methodChannelnewMethodChannel(binding.getBinaryMessenger(),drag_and_drop_flutter/ohos);this.methodChannel.setMethodCallHandler(this);this.eventChannelnewEventChannel(binding.getBinaryMessenger(),drag_and_drop_flutter/ohos/events);this.eventChannel.setStreamHandler(this);}6.3 生命周期释放onDetachedFromEngine(_binding:FlutterPluginBinding):void{this.unregisterDragAndDropCallbacks();this.methodChannel?.setMethodCallHandler(null);this.eventChannel?.setStreamHandler(null);this.methodChannelnull;this.eventChannelnull;this.sinknull;}6.4 事件发送emitDrop(state:string,records:Arrayany):void{this.sink?.success({state,items:UdmfDropNormalizer.normalize(records),});}七、Flutter 侧实现7.1 Harmony 平台实现classDragAndDropFlutterOhosPlatformextendsDragAndDropFlutterPlatform{staticconstMethodChannel_methodsMethodChannel(drag_and_drop_flutter/ohos);staticconstEventChannel_eventsEventChannel(drag_and_drop_flutter/ohos/events);staticvoidregisterWith(){DragAndDropFlutterPlatform.instanceDragAndDropFlutterOhosPlatform();}}7.2 DropArea 构建overrideWidgetbuildDropArea({DragData?dragData,DataTransferTypeFilter?canDrop,DragEnterCallback?onDragEnter,DragExitCallback?onDragExit,DropCallback?onDrop,requiredWidgetchild,}){return_OhosDropArea(dragData:dragData,canDrop:canDrop,onDragEnter:onDragEnter,onDragExit:onDragExit,onDrop:onDrop,child:child,);}7.3 事件转换void_handleEvent(Object?event){finalpayloadMapdynamic,dynamic.from(eventasMap);finalstatepayload[state];if(stateenter){widget.onDragEnter?.call(_metadata(payload[items]));}elseif(stateexit){widget.onDragExit?.call();}elseif(statedrop){widget.onDrop?.call(DragData(readonly:true,items:_items(payload[items]),),);}}7.4 文件入口class_OhosFileEntryimplementsFilesystemEntry{const_OhosFileEntry(this.path,this.name);overridefinalStringpath;overridefinalStringname;}当前平台接口的FilesystemEntry只要求path和name。如果业务需要读取内容可以在后续版本增加 OpenHarmony 专用的 URI 导入方法。八、pubspec 配置8.1 主包配置flutter:plugin:platforms:android:dartPluginClass:NullDragAndDropPlatformios:dartPluginClass:NullDragAndDropPlatformweb:default_package:drag_and_drop_flutter_webohos:default_package:drag_and_drop_flutter_ohos8.2 鸿蒙联邦包配置name:drag_and_drop_flutter_ohosversion:0.1.0flutter:plugin:implements:drag_and_drop_flutterplatforms:ohos:pluginClass:DragAndDropFlutterOhosPluginfileName:drag_and_drop_flutter_ohos.dart8.3 本地开发覆盖发布包不能依赖本地路径因此仓库开发阶段将路径覆盖放在dependency_overridesdependency_overrides:drag_and_drop_flutter_ohos:path:../drag_and_drop_flutter_ohos九、文件和目录处理9.1 文件导入流程获取 URI ↓ 检查读取授权 ↓ 读取元数据 ↓ 复制到应用沙箱 ↓ 将沙箱路径交给业务层9.2 不要直接替换 URI错误写法constpathuri.replace(file://,);这种方式会破坏编码也无法处理非本地 URI。应该通过 OpenHarmony SDK 提供的 URI 文件访问接口完成读取。9.3 目录限制目录递归需要限制深度和数量interfaceDirectoryScanOptions{maxDepth:number;maxEntries:number;maxTotalBytes:number;}建议默认限制项目建议值最大递归深度20最大文件数10000最大总大小10 GB单次拖放记录数100十、安全处理10.1 外部数据校验functionvalidateDropItem(item:Recordstring,Object):boolean{consttypeString(item.type??);if(![text/plain,file,inode/directory].includes(type)){returnfalse;}if(type!text/plain!item.uri){returnfalse;}returntrue;}10.2 文件名清理functionsafeFileName(name:string):string{returnname.replace(/[\\/:*?|]/g,_).replace(/\.\./g,_).slice(0,255);}10.3 日志脱敏functionmaskUri(uri:string):string{if(uri.length12){return***;}return${uri.slice(0,8)}***${uri.slice(-4)};}跨应用拖放数据应当视为不可信输入。不要因为事件来自系统框架就跳过长度、类型、URI 和路径校验。十一、示例用法classDropPageextendsStatefulWidget{constDropPage({super.key});overrideStateDropPagecreateState()_DropPageState();}class_DropPageStateextendsStateDropPage{Stringmessage将文本、文件或目录拖到这里;overrideWidgetbuild(BuildContextcontext){returnDragDropArea(canDrop:(items)items.every((item){returnitem.typetext/plain||item.isFile;}),onDragEnter:(_){setState(()message可以释放);},onDragExit:(){setState(()message将文本、文件或目录拖到这里);},onDrop:(data){setState(()message收到${data.items.length}条数据);},child:Center(child:Text(message)),);}}十二、测试方案12.1 文本测试从文本编辑器拖入纯文本从浏览器拖入选中文本测试空文本和超长文本测试换行符和特殊字符检查 Dart 回调是否收到text/plain。12.2 文件测试单个文本文件多个文件图片和 PDF中文文件名重名文件无授权 URI大文件异步导入。12.3 目录测试空目录多级目录目录中包含不可读文件目录递归超限目录总大小超限。12.4 三方应用矩阵源应用文本文件目录文件管理器是是是文档应用是视授权而定视授权而定浏览器是视页面而定否另一个 Flutter 应用是是视源端实现自研 ArkTS 应用是是是十三、构建与验证13.1 Flutter 依赖cdpackages/drag_and_drop_flutter flutter pub get13.2 Dart 静态检查flutter analyze当前仓库已验证drag_and_drop_flutter No issues found drag_and_drop_flutter_ohos No issues found example No issues found13.3 DevEco 构建在 DevEco Studio 中打开packages/drag_and_drop_flutter/example/ohos然后执行依赖同步和默认构建任务。HAR 插件本身依赖本机 Flutter OpenHarmony 引擎产物路径需要根据本地 Flutter SDK 位置调整。十四、常见问题14.1 为什么不直接返回绝对路径因为跨应用拖放通常返回 URI。绝对路径可能不属于当前应用沙箱直接拼接还会引入路径穿越风险。14.2 为什么需要 EventChannel拖放事件由 ArkUI 和 Flutter 引擎异步产生EventChannel 更适合推送进入、移动、离开和释放事件。14.3 为什么canDrop在 Dart 层判断这样可以保持和原插件一致让业务层根据 MIME 类型和文件标志决定是否接受数据。14.4 为什么还要兼容extraParams不同引擎或 SDK 版本可能把拖放附加数据放在事件对象或额外参数中。适配层同时尝试事件对象和 JSON 字符串能提高跨版本兼容性。14.5 是否需要显示模式权限不需要。显示模式与drag_and_drop、UDMF、URI 访问是不同能力。十五、后续完善方向增加 OpenHarmony URI 到沙箱文件的异步复制 API增加文件复制进度和取消接口增加目录递归枚举接口增加图片和自定义 UDMF 类型针对不同 SDK 建立兼容测试矩阵在真实设备上补充跨应用测试截图和性能数据。图片说明发布文章时建议替换为实际 DevEco 工程截图、拖放时序图或设备运行截图。当前图片路径是占位位置不应伪装成已经生成的实机截图。总结本次适配以原插件平台接口为边界在 OpenHarmony 侧使用 Flutter 引擎内置的drag_and_drop框架并通过FlutterManager注册拖放回调数据层使用 UDMF 统一处理文本、文件和目录记录跨层使用 EventChannel 传递结构化结果。最终得到的工程具备以下特点不修改原有DragDropArea公共 API不需要申请显示模式权限支持文本、文件和目录拖放文件和目录使用 URI 表达原生层和 Dart 层职责清晰联邦插件和示例 OpenHarmony 工程结构完整可以在 DevEco 工程中继续绑定具体 SDK 的 URI 读取实现。如果这篇文章对你有帮助欢迎点赞、收藏、关注你的支持是我持续创作的动力相关资源drag_and_drop_flutterFlutter federated pluginsFlutter Platform ChannelsOpenHarmony 官方文档HarmonyOS 开发者文档OpenHarmony UDMF 文档OpenHarmony 拖放开发文档OpenHarmony Flutter 社区
返回列表