Flutter 三方库「drag_and_drop_flutter」的鸿蒙化适配指南
开发工具: 华为云码道
本文配套仓库(预定地址): oh-flutter/drag_and_drop_flutter
drag_and_drop_flutter 将原生拖放能力接入 Flutter 区域组件,应用可以接收文本、链接和文件,也可以通过长按发起系统拖拽。本文以 drag_and_drop_flutter 0.3.0 为例,介绍源码准备、OHOS 工程配置、ArkTS 实现和 example 真机运行。
OHOS 接入原生拖放,Web 使用独立实现包;Android 和 iOS 的当前注册项是 NullDragAndDropPlatform,不能据此宣称具有相同原生拖放能力。配套仓库地址统一规划为 oh-flutter/drag_and_drop_flutter;尚未创建或同步时,先使用本地源码。本文代码参考本地提交 0a0fc22f5ab3ddd07134e5f633e9a40ef6ab2faa;该提交需同步到配套仓库后才能通过远端获取。
截图标注:操作步骤与截图命令真机运行图(从左到右):拖放区域/释放后的实际结果/主动拖拽与悬停。采集于 2026-09-09,设备 ALN-AL00 / HUAWEI Mate 60 Pro,系统 OpenHarmony 6.1.1.120(API 24);使用本地当前源码构建的签名 Release HAP。
释放回调已触发,但文本内容为空;当前截图不代表数据传递正确。
执行目录:本文 Markdown 所在目录。先用 hdc list targets 获取设备 ID,将下列 <device-id> 替换为实际值。截图使用仓库完整 Demo;snapshot_display 在本机要求 .jpeg 后缀,图片保持原始真机画面。
mkdir -p blog-assets/drag_and_drop_flutter
拖放区域: 启动完整 Demo,保留 Drag out 和 Drop in 两个区域。
hdc -t <device-id> shell snapshot_display -f /data/local/tmp/drag_and_drop_flutter-entry.jpeg
hdc -t <device-id> file recv /data/local/tmp/drag_and_drop_flutter-entry.jpeg ./blog-assets/drag_and_drop_flutter/entry.jpeg
释放后的实际结果: 将本页 Drag out 的链接长按拖入 Drop in 后释放。当前版本出现完成图标,但两行 text/plain 后的内容为空。
hdc -t <device-id> shell snapshot_display -f /data/local/tmp/drag_and_drop_flutter-drop-result.jpeg
hdc -t <device-id> file recv /data/local/tmp/drag_and_drop_flutter-drop-result.jpeg ./blog-assets/drag_and_drop_flutter/drop-result.jpeg
主动拖拽与悬停: 长按 Drag out 的链接并向 Drop in 移动,在手指尚未释放时截图;画面包含原生拖拽标记和 Release to drop。
hdc -t <device-id> shell snapshot_display -f /data/local/tmp/drag_and_drop_flutter-drag-preview.jpeg
hdc -t <device-id> file recv /data/local/tmp/drag_and_drop_flutter-drag-preview.jpeg ./blog-assets/drag_and_drop_flutter/drag-preview.jpeg
一、插件简介与适配目标
系统拖放将数据与拖拽手势在应用之间传递。OHOS 原生侧通过 FlutterManager 拖放回调接收窗口坐标与 UDMF 数据,Dart 侧根据 DragDropArea 的布局范围路由到目标组件。
例如,编辑器可以接收拖入的文字,资料页面可以接收文件,链接卡片也可以从 Flutter 组件拖到支持该类型的应用。
入站拖拽由原生通过 MethodChannel 反向通知 Dart,出站拖拽由长按手势发起。它没有额外的 EventChannel;数据读取和来源应用授予的 URI 访问能力需要一并验证。
二、环境准备
环境搭建参考社区文档:Flutter OH 开发环境搭建,完成 Flutter OH SDK 安装、环境变量和 DevEco Studio 配置。
完成后,在宿主机终端执行以下命令,确认当前选中的是支持 OHOS 的 Flutter 工具链,并能发现目标设备:
flutter --version
flutter doctor -v
hdc list targets
本文使用的工具链和复现时补全的 SDK 配置如下:
| 项目 | 版本或配置 | 用途 |
|---|---|---|
| Flutter OHOS SDK | 3.44.9+ohos-0.0.1-canary1 | Flutter 编译与 OHOS 平台工具链 |
| Flutter 分支 | oh-3.44.9-dev | CPF-Flutter 对应开发分支 |
| Dart SDK | 3.12.2 | Dart 语言与包管理环境 |
| HarmonyOS 开发套件 | 7.0.0(API 26) | 开发套件版本及对应的 API 级别 |
compileSdkVersion | 26.0.0(本文补全) | 编译时使用的 SDK API |
targetSdkVersion | 26.0.0(本文补全) | 应用面向的行为版本 |
compatibleSdkVersion | 5.1.0(18) | 当前工程声明的最低兼容版本 |
| 插件版本 | 0.3.0 | pubspec.yaml 中的包版本 |
| 原生语言 | ArkTS | HarmonyOS 插件实现 |
| 插件产物 | HAR | 被应用 entry 模块依赖 |
2.1 开发套件版本与工程中的 SDK 版本配置
7.0.0(API 26) 和 26.0.0 涉及 HarmonyOS 开发套件版本(API 版本)及其底座 OpenHarmony 的版本号体系。在本文工程中,相关版本的含义与配置方式如下:
7.0.0(API 26)表示 HarmonyOS 开发套件版本为7.0.0,对应 API 26。26.0.0是本文 HarmonyOS 应用工程中compileSdkVersion和targetSdkVersion的属性值。5.1.0(18)是本文工程中compatibleSdkVersion的属性值,声明最低兼容 API 18。
本地示例未显式填写 compileSdkVersion、targetSdkVersion。下面为沿用 API 26 工具链时需合并的 product 片段,补全值不代表原文件已包含这些字段:
{
"name": "default",
"compatibleSdkVersion": "5.1.0(18)",
"compileSdkVersion": "26.0.0",
"targetSdkVersion": "26.0.0",
"runtimeOS": "HarmonyOS"
}
这组配置使用 API 26 SDK 编译,并以 API 26 为目标版本,最低兼容 API 18。拖放依赖来源与目标应用的 UDMF 支持和 URI 授权。文件复制失败时实现可能保留原 URI,不能保证每个拖入文件都已转成可持久访问的本地文件。
三、从源码仓库开始准备适配工程
3.1 将上游源码同步到 AtomGit
适配已有第三方插件时,先从 Pub 包信息或项目 README 确认上游地址、包名、版本和许可证。同步仓库时保留源码、许可证和提交历史,方便后续跟踪上游更新。
在 AtomGit 网页的新建仓库流程中,找到从已有仓库导入的入口,填写上游 Git 地址,选择自己有写权限的组织或个人空间,设置目标仓库名称后执行导入。完成后检查目标分支、pubspec.yaml、LICENSE 和提交历史是否完整,并记录适配起点的提交号。如果上游本身托管在 AtomGit,也可以通过 Fork 获得自己的工作仓库。
上游源码为 https://github.com/Jjagg/drag_and_drop_flutter/tree/main/packages/drag_and_drop_flutter,本文基于 0.3.0。配套仓库预定为 drag_and_drop_flutter。仓库创建并同步适配代码后,再执行下面的拉取命令;尚未同步时使用本地副本。需要提交修改时,使用自己有写权限的仓库或 Fork。
3.2 将代码拉取到宿主机
在安装了 Flutter OH 和 DevEco Studio 的开发电脑上打开终端,进入准备存放项目的目录,执行:
git clone https://atomgit.com/oh-flutter/drag_and_drop_flutter.git
cd drag_and_drop_flutter
pwd
ls
git remote -v
git status --short --branch
git rev-parse HEAD
git clone 会创建 drag_and_drop_flutter/ 仓库目录。目标 Dart 包位于 packages/drag_and_drop_flutter/;以下章节中,“插件根目录”指该包目录,其中应能看到 pubspec.yaml、lib/ 和 example/。Git 仓库名和 Dart 包名均为 drag_and_drop_flutter。
需要使用与本文相同的代码版本时,先确认配套仓库已经包含下列本地参考提交,再在没有未提交修改的仓库中执行:
git switch --detach 0a0fc22f5ab3ddd07134e5f633e9a40ef6ab2faa
适配其他插件时,使用该插件对应版本的 tag 或 commit 作为分支起点。

图 1:在宿主机终端输入 AtomGit 仓库拉取命令。
3.3 在仓库根目录创建适配分支
接着在 drag_and_drop_flutter/ 根目录创建分支。分支名统一使用 feat/ohos_库名称_版本号,库名称取 packages/drag_and_drop_flutter/pubspec.yaml 的 name,版本号取此次适配的基线版本。本例为:
git switch -c feat/ohos_drag_and_drop_flutter_0.3.0
git branch --show-current
如果该分支已存在,使用 git switch feat/ohos_drag_and_drop_flutter_0.3.0 切换即可。

图 2:在 drag_and_drop_flutter 仓库根目录输入适配分支创建命令。
3.4 自动补全 OHOS 适配结构
分支在 Git 仓库根目录创建;随后执行 cd packages/drag_and_drop_flutter 进入插件包根目录,再执行结构补全。以下命令适用于尚无 ohos/ 目录的既有 Flutter 平台插件:
flutter create --template=plugin --platforms=ohos --project-name drag_and_drop_flutter .
git status --short
git diff -- pubspec.yaml lib example
--template=plugin指定插件模板。--platforms=ohos指定需要补全的平台。--project-name drag_and_drop_flutter使用 Dart 包名,避免当前目录重命名后生成错误的包名。- 最后的
.表示在当前插件目录补全工程,不是另建一层drag_and_drop_flutter/。
该命令生成 OHOS 平台脚手架,业务逻辑需要在 ArkTS 中实现。生成后通过 diff 检查 pubspec.yaml、lib/ 和 example/ 的变化,保留已有 API、其他平台注册项及依赖配置。不同 Flutter OH 版本生成的模板可能略有差异。
如果生成后 example/ohos/ 仍不存在,进入已有示例应用补全平台:
cd example
flutter create --platforms=ohos .
cd ..
本地适配工作区已经包含 ohos/ 和 example/ohos/,直接运行示例时可以跳过结构补全。新建插件则使用 flutter create --org com.nutpi --template=plugin --platforms=ohos drag_and_drop_flutter;已有插件使用上面的 . 在当前目录补全。

图 3:在插件根目录输入 OHOS 结构补全命令。
3.5 适配后的项目目录
适配后的关键目录如下:
drag_and_drop_flutter/packages/drag_and_drop_flutter/
├── lib/
│ ├── drag_and_drop_flutter.dart
│ └── src/
│ ├── drag_drop_area.dart
│ └── drag_and_drop_flutter_ohos.dart
├── ohos/
│ ├── src/
│ │ └── main/
│ │ ├── ets/
│ │ │ └── components/
│ │ │ └── plugin/
│ │ │ └── DragAndDropFlutterPlugin.ets
│ │ └── module.json5
│ ├── index.ets
│ └── oh-package.json5
├── example/
│ ├── lib/
│ │ └── main.dart
│ └── ohos/
│ ├── entry/
│ │ └── src/
│ │ └── main/
│ │ └── module.json5
│ └── build-profile.json5
├── pubspec.yaml
├── README.OpenHarmony_CN.md
├── README.OpenHarmony.md
├── CHANGELOG.OpenHarmony.md
└── test/
项目根目录如下,其中包含 ohos/、example/ 及实际保留的说明文件;交付文档清单见第六节:

图 4:适配后的 drag_and_drop_flutter 插件包目录。
| 文件 | 主要职责 |
|---|---|
lib/drag_and_drop_flutter.dart | 提供业务公开 API |
lib/src/drag_drop_area.dart | 实现平台协议或数据模型 |
lib/src/drag_and_drop_flutter_ohos.dart | 实现平台协议或数据模型 |
ohos/src/main/ets/components/plugin/DragAndDropFlutterPlugin.ets | 注册通道并实现 OHOS 原生能力 |
ohos/src/main/module.json5 | 声明 HAR 模块和权限 |
example/lib/main.dart | 演示接口调用与结果显示 |
四、Dart 接口与通道分析
OHOS 实现需要遵循 Dart 层已有的方法、参数和回调约定。先阅读 lib/drag_and_drop_flutter.dart、lib/src/drag_drop_area.dart、lib/src/drag_and_drop_flutter_ohos.dart,再在 ohos/src/main/ets/components/plugin/DragAndDropFlutterPlugin.ets 中实现对应的原生调用。适配其他已有平台的插件时,也应保留其公开接口和其他平台实现。
本例的对应关系如下:
| Dart 入口或模型 | 通道协议 | OHOS 实现 | 应保持的行为 |
|---|---|---|---|
DragDropArea | 原生反向 onDragEnter/onDragMove/onDragLeave/onDrop | FlutterManager 拖放回调 | 按区域调用业务回调 |
dragData + 长按 | startDrag | UIContext DragController | 生成 UDMF 与预览图 |
canDrop | setDropEnabled | 更新 DragResult | 异步反馈接收意图 |
DirectoryEntry.getEntries() | getDirectoryEntries | 文件系统与 URI 读取 | 直接子项或异常 |
原生端需要保持方法名、参数键和返回类型一致,不能只保留方法名称而改变业务语义。
4.1 跨端架构与调用时序
同一 MethodChannel 双向传递协议。Dart 调用 setDropEnabled、startDrag 和 getDirectoryEntries;原生反向调用 onDragEnter、onDragMove、onDragLeave、onDrop。区域命中使用窗口坐标转换后的组件范围。
4.1.1 一次完整接收拖放的时序
4.2 数据模型:MIME 元数据与 DragData
| 模型 | 字段或用途 | 边界 |
|---|---|---|
DataTransferItemMetadata | type、isFile | 进入区域阶段只有元数据 |
DataTransferItem | type、data 或 file | 放下后才处理完整内容 |
FileEntry / DirectoryEntry | 文件或目录对象 | 目录枚举受 URI 权限约束 |
DragData.type | copy、move、link | OHOS 出站无法强制操作类型 |
DragDropType? _decodeOperation(dynamic operation) {
switch (operation) {
case 'copy':
return DragDropType.copy;
case 'move':
return DragDropType.move;
case 'link':
return DragDropType.link;
}
return null;
}
未知 operation 按 null 处理;文本、HTML、链接、自定义 MIME 与文件使用不同解码路径。
4.3 公开 API 与平台接口
公开区域组件将参数交给平台实现。需要 onDrop 才会接收数据;dragData 非空时允许发起拖拽。
Widget buildDropArea({
DragData? dragData,
DataTransferTypeFilter? canDrop,
DragEnterCallback? onDragEnter,
DragExitCallback? onDragExit,
DropCallback? onDrop,
required Widget child,
}) {
if (dragData == null &&
onDragEnter == null &&
onDragExit == null &&
onDrop == null) {
return child;
}
return _OhosDropArea(
platform: this,
dragData: dragData,
canDrop: canDrop,
onDragEnter: onDragEnter,
onDragExit: onDragExit,
onDrop: onDrop,
child: child,
);
}
平台接口包在仓库 packages/drag_and_drop_flutter_platform_interface;OHOS 实现保持 DataTransferItem、DragData、FilesystemEntry 模型。
4.4 Dart 通道协议分析
4.4.1 通道名称必须两端完全一致
const methodChannel = MethodChannel('drag_and_drop_flutter');
通道名称属于跨语言协议。Dart 和 ArkTS 任何一端拼写不一致,都会出现“方法未实现”或“调用找不到插件”等问题。
4.4.2 按区域路由拖入数据
Future<void> _handleMethodCall(MethodCall call) async {
final Map<dynamic, dynamic> arguments = Map<dynamic, dynamic>.from(
call.arguments as Map<dynamic, dynamic>,
);
switch (call.method) {
case 'onDragEnter':
case 'onDragMove':
_metadata = _decodeMetadata(arguments['items']);
_updateTarget(
Offset(
(arguments['x'] as num).toDouble(),
(arguments['y'] as num).toDouble(),
),
);
break;
case 'onDragLeave':
_leaveActiveArea();
break;
case 'onDrop':
_handleDrop(arguments);
break;
}
}
void _updateTarget(Offset position) {
_OhosDropAreaState? target;
for (final _OhosDropAreaState area in _areas.reversed) {
if (area._contains(position)) {
target = area;
break;
}
}
if (!identical(target, _activeArea)) {
_activeArea?._notifyDragExit();
_activeArea = target;
target?._notifyDragEnter(_metadata);
}
final bool enabled = target != null && target._canAccept(_metadata);
_setDropEnabled(enabled);
}
void _handleDrop(Map<dynamic, dynamic> arguments) {
final Offset position = Offset(
(arguments['x'] as num).toDouble(),
(arguments['y'] as num).toDouble(),
);
_updateTarget(position);
final List<DataTransferItem> items = _decodeItems(arguments['items']);
final DragDropType? operation = _decodeOperation(arguments['operation']);
final DragData data = DragData(
readonly: true,
type: operation,
items: items,
);
final _OhosDropAreaState? target = _activeArea;
final List<DataTransferItemMetadata> metadata = items
.map<DataTransferItemMetadata>(
(DataTransferItem item) =>
DataTransferItemMetadata(type: item.type, isFile: item.isFile),
)
.toList(growable: false);
if (target != null && target._canAccept(metadata)) {
target._notifyDrop(data);
} else {
target?._notifyDragExit();
}
_activeArea = null;
_metadata = const <DataTransferItemMetadata>[];
_setDropEnabled(false);
}
按已注册区域的逆序寻找命中目标。canDrop 返回结果经过异步通道反馈给原生,首次进入时系统光标可能晚一个 move 才更新。
4.4.3 主动拖拽与移除区域
Future<void> _startDrag(DragData dragData, int pointerId) async {
final List<Map<String, dynamic>> items =
dragData.items.map<Map<String, dynamic>>((DataTransferItem item) {
final FilesystemEntry? entry = item.file;
return <String, dynamic>{
'type': item.type,
'isFile': item.isFile,
'isDirectory': entry is DirectoryEntry,
'path': entry?.path,
'data': item.data,
};
}).toList(growable: false);
try {
await _channel.invokeMethod<bool>('startDrag', <String, dynamic>{
'operation': _encodeOperation(dragData.type),
'pointerId': pointerId,
'items': items,
});
} on PlatformException catch (error) {
debugPrint('OpenHarmony drag could not start: ${error.message}');
}
}
void _unregisterArea(_OhosDropAreaState area) {
_areas.remove(area);
if (identical(_activeArea, area)) {
_activeArea = null;
_setDropEnabled(false);
}
}
当前代码在 onPointerDown 记录 PointerDownEvent.embedderId,长按时传入原生,原生要求 0–9 的有效 pointerId。文档中旧的固定 pointerId: 0 描述不适用于本参考代码。页面组件 dispose 自动注销区域。
五、补全 OHOS 原生实现与工程配置
5.1 在 DragAndDropFlutterPlugin.ets 中实现原生能力
业务层沿用已有 API,原生侧在 DragAndDropFlutterPlugin 中接入 UDMF 与 ArkUI DragController,通过 Flutter 通道回传结果。
下面列出类中的主要成员和方法,完整文件还包含 getUniqueClassName() 等插件接口;各段均为核心摘录,需要结合完整类使用。
原生插件位于:
ohos/src/main/ets/components/plugin/DragAndDropFlutterPlugin.ets
5.1.1 引入 Flutter 和 HarmonyOS 能力
import {
AbilityAware,
AbilityPluginBinding,
Any,
DragDropCallback,
FlutterManager,
FlutterPlugin,
FlutterPluginBinding,
Log,
MethodCall,
MethodCallHandler,
MethodChannel,
MethodResult,
} from '@ohos/flutter_ohos';
import common from '@ohos.app.ability.common';
import dragController from '@ohos.arkui.dragController';
import UDC from '@ohos.data.unifiedDataChannel';
import UTD from '@ohos.data.uniformTypeDescriptor';
import fileUri from '@ohos.file.fileuri';
import fs from '@ohos.file.fs';
import image from '@ohos.multimedia.image';
import util from '@ohos.util';
FlutterPlugin 负责接入 Flutter Engine 生命周期,MethodChannel 接收 Dart 命令;系统能力由 UDMF 与 ArkUI DragController 提供。错误和事件处理以对应方法实现为准。
5.1.2 连接 Flutter Engine 和宿主 Ability
private channel: MethodChannel | null = null;
private applicationContext: common.Context | null = null;
private abilityBinding: AbilityPluginBinding | null = null;
private dropEnabled: boolean = false;
private dragEnterCallbackId: number = -1;
private dragMoveCallbackId: number = -1;
private dragLeaveCallbackId: number = -1;
private dropCallbackId: number = -1;
private dragAction: dragController.DragAction | null = null;
private dragPreview: image.PixelMap | null = null;
private cachedFiles: Array<string> = [];
private cachedFileId: number = 0;
onAttachedToEngine(binding: FlutterPluginBinding): void {
this.applicationContext = binding.getApplicationContext();
this.channel = new MethodChannel(binding.getBinaryMessenger(), CHANNEL_NAME);
this.channel.setMethodCallHandler(this);
const manager = FlutterManager.getInstance();
this.dragEnterCallbackId = manager.addDragEnterCb(new DragEnterCallback(this));
this.dragMoveCallbackId = manager.addDragMoveCb(new DragMoveCallback(this));
this.dragLeaveCallbackId = manager.addDragLeaveCb(new DragLeaveCallback(this));
this.dropCallbackId = manager.addDropCb(new DropCallback(this));
}
onAttachedToAbility(binding: AbilityPluginBinding): void {
this.abilityBinding = binding;
}
onDetachedFromAbility(): void {
this.abilityBinding = null;
}
Engine 注册四个全局拖放回调并记录 ID,Ability 提供主动拖拽所需的主窗口。区域组件的注册表在 Dart 侧维护,两类注册不能混为一谈。
5.1.3 接收系统拖放数据
handleDragUpdate(method: string, event: DragEvent): void {
if (this.dropEnabled) {
event.setResult(DragResult.DROP_ENABLED);
} else {
event.setResult(DragResult.DROP_DISABLED);
}
this.channel?.invokeMethod(method, {
'x': event.getWindowX(),
'y': event.getWindowY(),
'items': this.getMetadata(event),
});
}
handleDrop(event: DragEvent): void {
event.setResult(this.dropEnabled ? DragResult.DRAG_SUCCESSFUL : DragResult.DRAG_FAILED);
const operation = event.dragBehavior === DragBehavior.MOVE ? 'move' : 'copy';
let items: Array<Any> = [];
try {
items = this.decodeRecords(event.getData().getRecords());
} catch (_error) {
items = [];
}
this.channel?.invokeMethod('onDrop', {
'x': event.getWindowX(),
'y': event.getWindowY(),
'operation': operation,
'items': items,
});
this.dropEnabled = false;
}
private decodeRecords(records: Array<UDC.UnifiedRecord>): Array<Any> {
const items: Array<Any> = [];
records.forEach((record: UDC.UnifiedRecord) => {
const type = record.getType();
if (this.isFileType(type)) {
const uri = this.getRecordUri(record, type);
if (uri.length > 0) {
const name = this.getName(uri);
const directory = this.isDirectoryType(type);
const path = directory ? uri : this.cacheDroppedFile(uri, name);
items.push({
'type': this.toMimeType(type, uri),
'isFile': true,
'isDirectory': directory,
'path': path,
'name': name,
});
}
return;
}
let data: string | null = null;
let mimeType = this.toMimeType(type);
if (type === UTD.UniformDataType.PLAIN_TEXT) {
data = (record as UDC.PlainText).textContent;
} else if (type === UTD.UniformDataType.HYPERLINK) {
data = (record as UDC.Hyperlink).url;
} else if (type === UTD.UniformDataType.HTML) {
data = (record as UDC.HTML).htmlContent;
} else if (type.startsWith('ApplicationDefined.') ||
record instanceof UDC.ApplicationDefinedRecord) {
const custom = record as UDC.ApplicationDefinedRecord;
data = util.TextDecoder.create('utf-8').decodeToString(custom.rawData);
if (custom.applicationDefinedType.length > 0) {
mimeType = this.decodeApplicationDefinedType(custom.applicationDefinedType);
}
} else {
const value = record.getValue();
if (typeof value === 'string') {
data = value;
}
}
if (data !== null) {
items.push({
'type': mimeType,
'isFile': false,
'data': data,
});
}
});
return items;
}
元数据与完整 UDMF 记录分开转换。handleDrop 读取失败时传回空 items,不能把空列表一定解释为用户没有拖数据。
5.1.4 发起拖拽并清理预览
private startDrag(call: MethodCall, result: MethodResult): void {
Log.i(TAG, 'Starting an outbound drag.');
if (this.dragAction !== null) {
result.error('DRAG_IN_PROGRESS', 'A drag operation is already active.', null);
return;
}
if (this.applicationContext === null) {
result.error('NO_CONTEXT', 'The Flutter engine is not attached.', null);
return;
}
if (this.abilityBinding === null) {
result.error('NO_ACTIVITY', 'A foreground UIAbility is required to start a drag.', null);
return;
}
const ability = this.abilityBinding.getAbility();
const mainWindow = FlutterManager.getInstance().windowStageOf(ability)?.getMainWindowSync();
if (mainWindow === undefined) {
result.error('NO_WINDOW', 'The foreground UIAbility has no main window.', null);
return;
}
const items: Array<Any> = call.argument('items') as Array<Any>;
const pointerId: number = call.argument('pointerId') as number;
if (!Number.isInteger(pointerId) || pointerId < 0 || pointerId > 9) {
result.error('INVALID_POINTER_ID', 'The active pointer ID must be between 0 and 9.', null);
return;
}
const data = this.createUnifiedData(items);
this.createPreview().then((preview: image.PixelMap) => {
this.dragPreview = preview;
const dragInfo: dragController.DragInfo = {
pointerId: pointerId,
data: data,
extraParams: '',
previewOptions: {
numberBadge: false,
mode: DragPreviewMode.DISABLE_SCALE,
},
};
const dragItem: DragItemInfo = {
pixelMap: this.dragPreview!,
extraInfo: 'drag_and_drop_flutter',
};
const action = mainWindow.getUIContext().getDragController().createDragAction(
[dragItem],
dragInfo,
);
this.dragAction = action;
action.on('statusChange', (info: dragController.DragAndDropInfo) => {
if (info.status === dragController.DragStatus.ENDED) {
this.releaseDragResources();
}
});
return action.startDrag();
}).then(() => {
Log.i(TAG, 'Outbound drag started.');
result.success(true);
}).catch((error: Error) => {
Log.e(TAG, 'Outbound drag failed.', error);
this.releaseDragResources();
this.reportError(result, 'START_DRAG_FAILED', error);
});
}
private releaseDragResources(): void {
if (this.dragAction !== null) {
this.dragAction.off('statusChange');
this.dragAction = null;
}
if (this.dragPreview !== null) {
this.dragPreview.release().catch((_error: Error) => {});
this.dragPreview = null;
}
}
预览是插件生成的 PixelMap,不是 Flutter 子组件截图;DragData.type 不会强制系统使用 move 或 link。ENDED 或启动失败后释放预览与 DragAction 引用。
5.1.5 处理 MethodChannel 命令
onMethodCall(call: MethodCall, result: MethodResult): void {
try {
switch (call.method) {
case 'setDropEnabled':
this.dropEnabled = Boolean(call.args);
result.success(null);
return;
case 'startDrag':
this.startDrag(call, result);
return;
case 'getDirectoryEntries':
result.success(this.getDirectoryEntries(String(call.args ?? '')));
return;
default:
result.notImplemented();
}
} catch (error) {
this.reportError(result, 'DRAG_AND_DROP_ERROR', error);
}
}
方法调用异常统一经 reportError 返回 DRAG_AND_DROP_ERROR;主动拖拽包含 DRAG_IN_PROGRESS、NO_CONTEXT、NO_ACTIVITY、NO_WINDOW、INVALID_POINTER_ID 等错误,异步启动失败返回 START_DRAG_FAILED。Dart 的主动拖拽实现捕获 PlatformException 并写 debugPrint。
5.1.6 Engine 解绑时释放资源
onDetachedFromEngine(_binding: FlutterPluginBinding): void {
this.channel?.setMethodCallHandler(null);
this.channel = null;
this.applicationContext = null;
this.abilityBinding = null;
this.dropEnabled = false;
const manager = FlutterManager.getInstance();
manager.removeDragEnterCb(this.dragEnterCallbackId);
manager.removeDragMoveCb(this.dragMoveCallbackId);
manager.removeDragLeaveCb(this.dragLeaveCallbackId);
manager.removeDropCb(this.dropCallbackId);
this.dragEnterCallbackId = -1;
this.dragMoveCallbackId = -1;
this.dragLeaveCallbackId = -1;
this.dropCallbackId = -1;
this.releaseDragResources();
this.releaseCachedFiles();
}
Engine 解绑移除四个全局回调,释放 DragAction、预览图与缓存文件。业务如需长期保留文件,应在缓存有效期间复制到自己的持久目录。
5.2 声明插件和宿主权限
插件 HAR 不声明权限。示例 entry 保留 INTERNET 供示例调试使用;跨应用文件读取仍依赖来源应用的 URI 授权,权限数组为空不代表可以读取任意文件。
5.2.1 插件 HAR 的权限
插件 ohos/src/main/module.json5 的模块配置如下:
{
"module": {
"name": "drag_and_drop_flutter",
"type": "har",
"deviceTypes": [
"default",
"tablet"
]
}
}
5.2.2 应用 entry 的权限
最终安装的是宿主应用。以下片段来自 example/ohos/entry/src/main/module.json5,合并时保留原有 Ability 等配置:
{
"module": {
"requestPermissions": [
{
"name": "ohos.permission.INTERNET"
}
]
}
}
权限声明与运行时授权需要分别处理;没有权限需求的功能不要套用其他插件的授权流程。
5.3 注册并导出插件
pubspec.yaml 通过以下配置声明 OHOS 插件类:
flutter:
plugin:
platforms:
ohos:
pluginClass: DragAndDropFlutterPlugin
dartPluginClass: DragAndDropFlutterOhosPlatform
dartFileName: src/drag_and_drop_flutter_ohos.dart
插件的 ohos/index.ets 需要导出实现:
import DragAndDropFlutterPlugin from './src/main/ets/components/plugin/DragAndDropFlutterPlugin';
export default DragAndDropFlutterPlugin;
执行 flutter pub get 和构建后,Flutter 工具会为应用生成插件注册代码。通常不应手工编辑 GeneratedPluginRegistrant.ets,因为下次构建可能覆盖它。
OHOS 注册同时包含 pluginClass、dartPluginClass 和 dartFileName。Dart 默认平台选择也提供 OHOS 回退实现;不要手工改 generated_plugin_registrant.dart 来替代插件配置。
注册异常的排查步骤见第九节 MissingPluginException。
5.4 检查 example 的 OHOS 应用结构
本例的 example/ohos/build-profile.json5 应在 products 中设置版本。下面按第二节补全 compileSdkVersion、targetSdkVersion,请合并到现有工程;其中 signingConfig: "default" 需要与本机配置的签名名称一致,签名材料保留在本地。
{
"app": {
"products": [
{
"name": "default",
"signingConfig": "default",
"compatibleSdkVersion": "5.1.0(18)",
"runtimeOS": "HarmonyOS",
"compileSdkVersion": "26.0.0",
"targetSdkVersion": "26.0.0"
}
]
},
"modules": [
{
"name": "entry",
"srcPath": "./entry",
"targets": [
{
"name": "default",
"applyToProducts": [
"default"
]
}
]
}
]
}
配置后,在 DevEco Studio 中执行一次 Sync Project。如果 entry 模块正常识别,Project 视图中会出现 entry,并能打开 entry 模块的签名配置。
六、补全交付文件并提交适配分支
6.1 除代码外还要补全哪些文件
代码适配完成后,还需要整理安装说明、接口文档、版本记录和开源信息。接收仓库有专用模板时,按其格式填写:
| 文件 | 应写清楚的内容 |
|---|---|
README.OpenSource | 上游名称、源码地址、适配版本或提交、版权及许可证信息;按仓库模板列出第三方依赖 |
README.md | 原项目说明、OHOS 支持入口、配套 Demo 和文档链接;保留上游信息 |
README.OpenHarmony_CN.md | 简介、AtomGit 安装方式、版本对应关系、环境约束、权限、接口表、示例、已验证范围和遗留问题 |
README.OpenHarmony.md | 与中文说明对应的英文文档 |
CHANGELOG.OpenHarmony.md | OHOS 新增能力、适配版本、兼容限制与测试范围 |
LICENSE / NOTICE | 保留上游许可证;NOTICE 按许可证和原项目要求保留或补充 |
example/README.md | 依赖方式、运行目录、签名、操作步骤与效果图;覆盖拖入、拒绝放下、主动拖出、文件读取和目录枚举 |
pubspec.yaml、ohos/oh-package.json5 | 核对包名、版本、插件注册、仓库地址、许可证和依赖 |
.gitignore | 忽略构建缓存及本机签名材料,不漏提交必要源码和配置 |
README.OpenSource 记录库本身的来源与版本。本例的包名为 drag_and_drop_flutter,版本为 0.3.0,采用 MIT 许可证;Flutter 和 HarmonyOS SDK 版本写入环境说明。
现有说明文件以目录树为准。README.OpenSource 等缺失交付文件按接收仓库要求补全;安装与反馈链接统一替换为本文预定的 AtomGit 地址,并在仓库创建、适配代码同步后核对。
6.2 提交前检查
提交前先完成第八节的插件、example 和真机测试,再从 Git 仓库根目录检查改动:
git branch --show-current
git diff --check
git status --short
git diff --stat
git diff
检查 diff 中的接口、平台注册和依赖变化,移除本机路径及签名信息,并使文档中的版本和分支与提交内容一致。
6.3 提交并推送到 AtomGit
文档和代码整理完成后,在 Git 仓库根目录暂存并提交。以下命令以第 6.1 节文档已经补全为前提,文件名按项目实际情况调整:
git add packages/drag_and_drop_flutter/lib packages/drag_and_drop_flutter/ohos packages/drag_and_drop_flutter/pubspec.yaml packages/drag_and_drop_flutter/example packages/drag_and_drop_flutter/test
git add packages/drag_and_drop_flutter/README.md packages/drag_and_drop_flutter/README.OpenSource packages/drag_and_drop_flutter/README.OpenHarmony_CN.md
git add packages/drag_and_drop_flutter/README.OpenHarmony.md packages/drag_and_drop_flutter/CHANGELOG.OpenHarmony.md
git diff --cached --check
git diff --cached --stat
git diff --cached
git commit -m "feat: add OHOS support for drag_and_drop_flutter 0.3.0"
git remote -v
git branch --show-current
git push -u origin feat/ohos_drag_and_drop_flutter_0.3.0
DevEco 可能向 example/ohos/build-profile.json5 写入本机签名配置,需要在提交前从暂存内容中移除。推送时,origin 应指向自己的 AtomGit 仓库,当前分支为 feat/ohos_drag_and_drop_flutter_0.3.0。
推送后在 AtomGit 发起合并请求,说明上游来源和版本、OHOS 实现范围、依赖及权限、测试环境、操作结果、已知限制,并附 Demo 运行图。目标分支和评审流程以接收仓库要求为准。
七、使用插件包内 example 演示接入
插件包自带 example/,可以直接用来调试插件和体验原生拖入与拖出。
7.1 本地适配时使用路径依赖
当前 example/pubspec.yaml 的依赖是:
dependencies:
flutter:
sdk: flutter
drag_and_drop_flutter:
path: ../
../ 相对于 example/pubspec.yaml 指向插件根目录,修改根目录插件后可直接联调。
7.2 通过 AtomGit 引入插件
业务应用通过 AtomGit 引入时,将 drag_and_drop_flutter 的 path 配置替换为下面的 Git 依赖。仓库同步后,可固定到本文的本地参考提交:
dependencies:
flutter:
sdk: flutter
drag_and_drop_flutter:
git:
url: https://atomgit.com/oh-flutter/drag_and_drop_flutter.git
ref: 0a0fc22f5ab3ddd07134e5f633e9a40ef6ab2faa
path: packages/drag_and_drop_flutter
使用自己的适配版本时,先推送分支,再将 url 改为对应仓库,ref 改为 feat/ohos_drag_and_drop_flutter_0.3.0。正式发布后可固定到 tag 或 commit。
从插件根目录执行:
cd example
flutter pub get
flutter pub deps
检查 example/pubspec.lock 中 drag_and_drop_flutter 的来源为 git,并核对 url、ref 和 resolved-ref。同时检查没有 dependency_overrides 或 pubspec_overrides.yaml 将其覆盖回本地依赖,确认应用使用的是 Git 依赖。
7.3 调用接口实现原生拖入与拖出
下面的页面可用于插件包内的 example/lib/main.dart,是便于讲解的最小页面;仓库完整 Demo 的入口和布局可能不同,第八节截图与验收步骤以仓库完整 Demo 为准。
import 'package:flutter/material.dart';
import 'package:drag_and_drop_flutter/drag_and_drop_flutter.dart';
void main() => runApp(const MaterialApp(home: DragPage()));
class DragPage extends StatefulWidget {
const DragPage({super.key});
State<DragPage> createState() => _DragPageState();
}
class _DragPageState extends State<DragPage> {
String _result = '拖入演示文本,或长按区域拖出链接';
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('原生拖放')),
body: Padding(
padding: const EdgeInsets.all(20),
child: DragDropArea(
canDrop: (items) => items.every((item) => !item.isFile),
onDragEnter: (items) {
setState(() => _result = '进入:${items.map((e) => e.type).join(', ')}');
},
onDragExit: () => setState(() => _result = '已离开或拒绝放下'),
onDrop: (data) {
setState(() => _result = data.items.map((e) => '${e.type}: ${e.data}').join('\n'));
},
dragData: DragData.fromMap(
type: DragDropType.copy,
items: const {'text/uri-list': 'https://flutter.dev', 'text/plain': 'Flutter 拖放演示'},
),
child: Container(
constraints: const BoxConstraints.expand(),
color: Colors.green.shade50,
padding: const EdgeInsets.all(16),
child: Text(_result),
),
),
),
);
}
}
7.4 页面退出时注销拖放区域
DragDropArea 的 OHOS 状态对象在 dispose 中注销区域。业务异步读取文件或目录后更新界面仍需检查 mounted。Engine 解绑会删除插件缓存文件,不应把缓存路径作为长期文件引用保存。
八、验证、构建与鸿蒙设备运行效果
8.1 分别验证插件与 example
从插件仓库根目录执行:
flutter pub get
flutter analyze
flutter test
cd example
flutter pub get
flutter analyze
flutter test
现有 Dart 测试覆盖 OHOS 注册与回退、区域命中、文本/文件/目录解码和长按参数;example Widget 测试检查拖入与拖出两个区域。测试不替代跨应用 URI 授权与真实系统手势。Widget 测试应匹配实际保留的 Demo 页面;如果替换成第七节最小页面,也要相应调整原来的 UI 断言。
Dart 测试覆盖接口和页面逻辑,拖入、拒绝放下、主动拖出、文件读取和目录枚举还需要在鸿蒙设备上验证。以上为 Dart 测试复现命令,本次未重新执行这些测试;本次已重新构建、安装并运行签名 Release HAP,具体真机采集范围见 8.5。
8.2 确认设备连接
hdc list targets
flutter devices
设备首次连接电脑时,需要在手机端确认调试授权。列表为空时,检查 USB 连接、调试模式和电脑授权。
8.3 配置签名
真机安装的 HAP 通常需要有效签名。推荐使用 DevEco Studio 为 entry 模块配置自动签名:
- 用 DevEco Studio 打开
example/ohos,不是仓库根目录; - 等待工程 Sync 成功,确认 Project 视图中存在
entry模块; - 打开 File > Project Structure > Signing Configs;
- 为
defaultproduct 选择或生成签名; - 确认设备、应用包名、证书和 Profile 匹配;
- 再回到终端执行 Flutter 构建或运行。
签名材料保存在本机,公开仓库中只保留构建所需的通用配置。
8.4 运行示例
以下命令在 example/ 目录执行,将 <device-id> 替换为设备列表中的实际 ID:
flutter run -d <device-id>
也可以先构建 HAP:
flutter build hap --debug
典型产物位于:
example/ohos/entry/build/default/outputs/default/
目录中通常包含已签名和未签名 HAP。真机安装应选择与当前设备匹配的已签名产物。
本次真机截图使用签名 Release HAP。在插件包的 example/ 目录完成签名配置后执行:
flutter build hap --release
hdc -t <device-id> install -r build/ohos/hap/entry-default-signed.hap
hdc -t <device-id> shell aa start -a EntryAbility -b com.example.drag_and_drop_flutter_example
8.5 在设备上测试原生拖入与拖出
- 运行完整 Demo,确认 Drag and Drop 页面有 Drag out 和 Drop in 两个区域。
- 从支持系统拖放的来源应用拖入纯文本或链接,检查 Drop in 的内容与类型。
- 拖入 canDrop 不接受的类型,检查没有错误触发 onDrop。
- 拖入文件并读取内容,拖入目录并调用 getEntries,核对授权不足时的行为。
- 长按 Long press and drag this link,拖到支持链接的目标应用。
- 拖离区域、反复拖入拖出、退出重进,确认回调与预览释放正常。
现有 OpenHarmony 说明记录了 API 24 设备的安装与启动,并明确跨应用拖入、拖出需要配合验证。本文不将该记录扩展为全部拖放功能已经真机通过。
8.6 鸿蒙设备运行效果
OHOS 实现提供原生拖入与拖出。以下为仓库完整 Demo 的三张真机运行截图,按实际状态记录。
截图标注:操作步骤与截图命令真机运行图(从左到右):拖放区域/释放后的实际结果/主动拖拽与悬停。采集于 2026-09-09,设备 ALN-AL00 / HUAWEI Mate 60 Pro,系统 OpenHarmony 6.1.1.120(API 24);使用本地当前源码构建的签名 Release HAP。
释放回调已触发,但文本内容为空;当前截图不代表数据传递正确。
执行目录:本文 Markdown 所在目录。先用 hdc list targets 获取设备 ID,将下列 <device-id> 替换为实际值。截图使用仓库完整 Demo;snapshot_display 在本机要求 .jpeg 后缀,图片保持原始真机画面。
mkdir -p blog-assets/drag_and_drop_flutter
拖放区域: 启动完整 Demo,保留 Drag out 和 Drop in 两个区域。
hdc -t <device-id> shell snapshot_display -f /data/local/tmp/drag_and_drop_flutter-entry.jpeg
hdc -t <device-id> file recv /data/local/tmp/drag_and_drop_flutter-entry.jpeg ./blog-assets/drag_and_drop_flutter/entry.jpeg
释放后的实际结果: 将本页 Drag out 的链接长按拖入 Drop in 后释放。当前版本出现完成图标,但两行 text/plain 后的内容为空。
hdc -t <device-id> shell snapshot_display -f /data/local/tmp/drag_and_drop_flutter-drop-result.jpeg
hdc -t <device-id> file recv /data/local/tmp/drag_and_drop_flutter-drop-result.jpeg ./blog-assets/drag_and_drop_flutter/drop-result.jpeg
主动拖拽与悬停: 长按 Drag out 的链接并向 Drop in 移动,在手指尚未释放时截图;画面包含原生拖拽标记和 Release to drop。
hdc -t <device-id> shell snapshot_display -f /data/local/tmp/drag_and_drop_flutter-drag-preview.jpeg
hdc -t <device-id> file recv /data/local/tmp/drag_and_drop_flutter-drag-preview.jpeg ./blog-assets/drag_and_drop_flutter/drag-preview.jpeg
| 拖放区域 | 拖入结果 | 主动拖拽 |
|---|---|---|
| 两个区域可见 | 实际入站数据 | 出站预览;接收结果需另验 |
拖放依赖来源与目标应用的 UDMF 支持和 URI 授权。文件复制失败时实现可能保留原 URI,不能保证每个拖入文件都已转成可持久访问的本地文件。
九、FAQ:适配过程与使用问题
9.1 Missing SDK components
典型错误如下:
Missing SDK components. SDK path: ...,
missing components: toolchains,ets,js,native,previewer.
这个错误发生在 Hvigor 同步阶段。通常需要检查构建工具使用的 SDK 路径、组件是否完整,以及 Hvigor 与 SDK 的版本是否匹配。
处理顺序:
- 在 DevEco Studio SDK Manager 中确认 API 26 组件已经下载完整;
- 检查 Flutter 和 DevEco Studio 使用的 SDK 路径是否一致;
- 避免误用
/Applications/DevEco-Studio.app/Contents/sdk之类的不完整目录; - 确认 SDK 根目录下存在
toolchains、ets、js、native、previewer; - 执行
flutter config --ohos-sdk <正确路径>; - 重新执行
flutter doctor -v和 DevEco Studio Sync。
因为 Sync 和 Compile 首先读取 Mac 本地 SDK。手机 API 版本只在部署、安装和运行时参与兼容判断。即使完全不连接手机,本地 SDK 不完整时也会得到相同错误。
当前工程的 compatibleSdkVersion 是 API 18,因此 API 24 在安装版本门槛上是满足的;但功能是否可用还取决于目标系统能力、权限和运行环境,不能仅凭最低版本判断。
9.2 DevEco Studio 中看不到 entry 模块
插件的 ohos/ 目录是 HAR 模块,可安装应用的 entry 模块位于 example/ohos/entry。
请直接使用 DevEco Studio 打开:
drag_and_drop_flutter/packages/drag_and_drop_flutter/example/ohos
如果仍看不到 entry,先解决 SDK Sync 错误,再检查 example/ohos/build-profile.json5 的 modules 是否包含 ./entry。同步失败时,Project Structure 无法正确解析模块,签名界面也可能不显示 entry。
9.3 无法手动签名
签名配置依附于可构建的应用模块和 product。只有 HAR 插件模块、工程 Sync 失败,或者打开了错误目录时,DevEco Studio 都可能无法提供 entry 签名入口。
建议先确认:
- 打开的是
example/ohos; - SDK 组件完整并且 Sync 成功;
entry的模块类型为entry;defaultproduct 和 target 已正确关联;- 当前账号、证书和调试设备状态有效。
9.4 拖入区域却没有收到数据
先检查目标应用来源是否支持 UDMF、组件是否配置 onDrop、canDrop 是否接收对应 MIME。再核对坐标命中。读取完整数据失败会发送空 items,文件 URI 权限或来源格式也可能是原因。
9.5 MissingPluginException
这通常表示 Dart 通道找不到已注册的原生插件。新增原生插件后需要重新构建应用。从仓库根目录执行:
cd example
flutter clean
flutter pub get
flutter run -d <device-id>
如果仍然出现,检查自动生成的插件注册文件中是否包含 DragAndDropFlutterPlugin,同时核对 pubspec.yaml、ohos/index.ets 和 oh-package.json5。
9.6 首次进入显示不可放下,移动后才恢复
可接收状态要经 Dart canDrop 与 MethodChannel 往返同步。原生用上一次 dropEnabled 设置 DragResult,随后 move 会更新;这是异步反馈时序,不能靠重复注册区域修复。
9.7 编译成功但安装失败
常见原因包括:
- HAP 未签名或使用了错误的 Profile;
- 设备未加入调试设备列表;
- 包名与签名 Profile 不匹配;
- 安装包的
compatibleSdkVersion高于设备 API; - 手机上已经安装了使用不同证书签名的同包名应用。
根据安装错误码区分签名、版本和包名冲突,再处理对应配置。
9.8 flutter create 不认识 ohos,或包名不合法
先执行 flutter --version,确认使用的是 OHOS 版工具链,环境配置回到第二节的社区链接核对。包名报错时,确认当前目录包含目标插件的 pubspec.yaml,并显式传入 --project-name drag_and_drop_flutter;本例 Dart 包名为 drag_and_drop_flutter,多包仓库需先进入对应插件包目录。生成后检查 diff,再补充 ArkTS 业务实现。
9.9 AtomGit 依赖提示找不到分支或无权限
先检查 URL 是否指向已同步的目标仓库,再确认 feat/ohos_drag_and_drop_flutter_0.3.0 已推送。仓库未创建、适配分支未推送或提交未同步时,应先完成同步;不能直接使用仅存在本地的提交号。私有仓库还需在本机配置 Git 认证。
9.10 改了本地 ArkTS,Demo 为什么没变化
先检查 example/pubspec.yaml:Git 依赖读取远程提交,不会自动读取本地插件改动。本地联调切回 path: ../;测试远程版本则先提交推送,再更新依赖并核对 pubspec.lock 的 resolved-ref。原生代码变动后停止应用并重新构建运行,不能只做 Dart 热重载。
9.11 长按报 INVALID_POINTER_ID,或预览不像组件
当前实现从 PointerDownEvent.embedderId 获取实际指针并校验 0–9;不要沿用旧文档的固定 0 描述。预览由插件生成,暂不截取 child 外观,copy/move/link 最终也由系统和目标端决定。
相关链接
更多推荐
所有评论(0)