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: const Text('Drop here'),
)
平台接口中的 DataTransferItem 支持两类数据:
DataTransferItem.data:字符串数据,例如text/plain;DataTransferItem.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_ohos:
packages/
├── 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.md
3.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 回调注册
const manager = FlutterManager.getInstance();
const enterId = manager.addDragEnterCb({
do: (_event: DragEvent, extra: string): void => {
emitDrop('enter', recordsFrom(extra));
}
});
const dropId = manager.addDropCb({
do: (event: DragEvent, extra: string): void => {
emitDrop('drop', recordsFromEvent(event, extra));
}
});
组件销毁或停止监听时,需要移除回调:
manager.removeDragEnterCb(enterId);
manager.removeDropCb(dropId);
五、UDMF 数据归一化
5.1 统一数据模型
原插件的数据模型不要求 Flutter 了解 UDMF 具体类型,因此在 ArkTS 层做一次归一化:
type DropItem = Record<string, Object>;
export class UdmfDropNormalizer {
static normalize(records: Array<any>): DropItem[] {
return records.map((record: any): DropItem => {
const type = String(
record.dataType ?? record.mimeType ?? record.type ?? ''
);
const uri = String(record.uri ?? record.fileUri ?? '');
if (type === 'text/plain' || type === 'text') {
return {
type: 'text/plain',
data: String(record.data ?? record.text ?? ''),
isFile: false,
};
}
if (uri.length > 0) {
return {
type: record.isDirectory === true
? '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 插件声明
export default class DragAndDropFlutterOhosPlugin
implements FlutterPlugin, MethodCallHandler, StreamHandler {
private methodChannel: MethodChannel | null = null;
private eventChannel: EventChannel | null = null;
private sink: EventSink | null = null;
private callbackIds: number[] = [];
}
6.2 通道注册
onAttachedToEngine(binding: FlutterPluginBinding): void {
this.methodChannel = new MethodChannel(
binding.getBinaryMessenger(),
'drag_and_drop_flutter/ohos'
);
this.methodChannel.setMethodCallHandler(this);
this.eventChannel = new EventChannel(
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.methodChannel = null;
this.eventChannel = null;
this.sink = null;
}
6.4 事件发送
emitDrop(state: string, records: Array<any>): void {
this.sink?.success({
state,
items: UdmfDropNormalizer.normalize(records),
});
}
七、Flutter 侧实现
7.1 Harmony 平台实现
class DragAndDropFlutterOhosPlatform
extends DragAndDropFlutterPlatform {
static const MethodChannel _methods =
MethodChannel('drag_and_drop_flutter/ohos');
static const EventChannel _events =
EventChannel('drag_and_drop_flutter/ohos/events');
static void registerWith() {
DragAndDropFlutterPlatform.instance =
DragAndDropFlutterOhosPlatform();
}
}
7.2 DropArea 构建
Widget buildDropArea({
DragData? dragData,
DataTransferTypeFilter? canDrop,
DragEnterCallback? onDragEnter,
DragExitCallback? onDragExit,
DropCallback? onDrop,
required Widget child,
}) {
return _OhosDropArea(
dragData: dragData,
canDrop: canDrop,
onDragEnter: onDragEnter,
onDragExit: onDragExit,
onDrop: onDrop,
child: child,
);
}
7.3 事件转换
void _handleEvent(Object? event) {
final payload = Map<dynamic, dynamic>.from(event as Map);
final state = payload['state'];
if (state == 'enter') {
widget.onDragEnter?.call(_metadata(payload['items']));
} else if (state == 'exit') {
widget.onDragExit?.call();
} else if (state == 'drop') {
widget.onDrop?.call(
DragData(
readonly: true,
items: _items(payload['items']),
),
);
}
}
7.4 文件入口
class _OhosFileEntry implements FilesystemEntry {
const _OhosFileEntry(this.path, this.name);
final String path;
final String name;
}
当前平台接口的 FilesystemEntry 只要求 path 和 name。如果业务需要读取内容,可以在后续版本增加 OpenHarmony 专用的 URI 导入方法。
八、pubspec 配置
8.1 主包配置
flutter:
plugin:
platforms:
android:
dartPluginClass: NullDragAndDropPlatform
ios:
dartPluginClass: NullDragAndDropPlatform
web:
default_package: drag_and_drop_flutter_web
ohos:
default_package: drag_and_drop_flutter_ohos
8.2 鸿蒙联邦包配置
name: drag_and_drop_flutter_ohos
version: 0.1.0
flutter:
plugin:
implements: drag_and_drop_flutter
platforms:
ohos:
pluginClass: DragAndDropFlutterOhosPlugin
fileName: drag_and_drop_flutter_ohos.dart
8.3 本地开发覆盖
发布包不能依赖本地路径,因此仓库开发阶段将路径覆盖放在 dependency_overrides:
dependency_overrides:
drag_and_drop_flutter_ohos:
path: ../drag_and_drop_flutter_ohos
九、文件和目录处理
9.1 文件导入流程
获取 URI
↓
检查读取授权
↓
读取元数据
↓
复制到应用沙箱
↓
将沙箱路径交给业务层
9.2 不要直接替换 URI
错误写法:
const path = uri.replace('file://', '');
这种方式会破坏编码,也无法处理非本地 URI。应该通过 OpenHarmony SDK 提供的 URI 文件访问接口完成读取。
9.3 目录限制
目录递归需要限制深度和数量:
interface DirectoryScanOptions {
maxDepth: number;
maxEntries: number;
maxTotalBytes: number;
}
建议默认限制:
| 项目 | 建议值 |
|---|---|
| 最大递归深度 | 20 |
| 最大文件数 | 10000 |
| 最大总大小 | 10 GB |
| 单次拖放记录数 | 100 |
十、安全处理
10.1 外部数据校验
function validateDropItem(item: Record<string, Object>): boolean {
const type = String(item.type ?? '');
if (!['text/plain', 'file', 'inode/directory'].includes(type)) {
return false;
}
if (type !== 'text/plain' && !item.uri) {
return false;
}
return true;
}
10.2 文件名清理
function safeFileName(name: string): string {
return name
.replace(/[\\/:*?"<>|]/g, '_')
.replace(/\.\./g, '_')
.slice(0, 255);
}
10.3 日志脱敏
function maskUri(uri: string): string {
if (uri.length < 12) {
return '***';
}
return `${uri.slice(0, 8)}***${uri.slice(-4)}`;
}
跨应用拖放数据应当视为不可信输入。不要因为事件来自系统框架,就跳过长度、类型、URI 和路径校验。
十一、示例用法
class DropPage extends StatefulWidget {
const DropPage({super.key});
State<DropPage> createState() => _DropPageState();
}
class _DropPageState extends State<DropPage> {
String message = '将文本、文件或目录拖到这里';
Widget build(BuildContext context) {
return DragDropArea(
canDrop: (items) => items.every((item) {
return item.type == 'text/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 依赖
cd packages/drag_and_drop_flutter
flutter pub get
13.2 Dart 静态检查
flutter analyze
当前仓库已验证:
drag_and_drop_flutter No issues found
drag_and_drop_flutter_ohos No issues found
example No issues found
13.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 读取实现。
如果这篇文章对你有帮助,欢迎点赞、收藏、关注,你的支持是我持续创作的动力!
相关资源:
更多推荐

所有评论(0)