开发工具: 华为云码道

本文配套仓库(预定地址): 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;该提交需同步到配套仓库后才能通过远端获取。


drag_and_drop_flutter 鸿蒙真机:拖放区域 drag_and_drop_flutter 鸿蒙真机:释放后的实际结果 drag_and_drop_flutter 鸿蒙真机:主动拖拽与悬停

真机运行图(从左到右):拖放区域/释放后的实际结果/主动拖拽与悬停。采集于 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 SDK3.44.9+ohos-0.0.1-canary1Flutter 编译与 OHOS 平台工具链
Flutter 分支oh-3.44.9-devCPF-Flutter 对应开发分支
Dart SDK3.12.2Dart 语言与包管理环境
HarmonyOS 开发套件7.0.0(API 26)开发套件版本及对应的 API 级别
compileSdkVersion26.0.0(本文补全)编译时使用的 SDK API
targetSdkVersion26.0.0(本文补全)应用面向的行为版本
compatibleSdkVersion5.1.0(18)当前工程声明的最低兼容版本
插件版本0.3.0pubspec.yaml 中的包版本
原生语言ArkTSHarmonyOS 插件实现
插件产物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 应用工程中 compileSdkVersiontargetSdkVersion 的属性值。
  • 5.1.0(18) 是本文工程中 compatibleSdkVersion 的属性值,声明最低兼容 API 18。

本地示例未显式填写 compileSdkVersiontargetSdkVersion。下面为沿用 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.yamlLICENSE 和提交历史是否完整,并记录适配起点的提交号。如果上游本身托管在 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.yamllib/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.yamlname,版本号取此次适配的基线版本。本例为:

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.yamllib/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.dartlib/src/drag_drop_area.dartlib/src/drag_and_drop_flutter_ohos.dart,再在 ohos/src/main/ets/components/plugin/DragAndDropFlutterPlugin.ets 中实现对应的原生调用。适配其他已有平台的插件时,也应保留其公开接口和其他平台实现。

本例的对应关系如下:

Dart 入口或模型通道协议OHOS 实现应保持的行为
DragDropArea原生反向 onDragEnter/onDragMove/onDragLeave/onDropFlutterManager 拖放回调按区域调用业务回调
dragData + 长按startDragUIContext DragController生成 UDMF 与预览图
canDropsetDropEnabled更新 DragResult异步反馈接收意图
DirectoryEntry.getEntries()getDirectoryEntries文件系统与 URI 读取直接子项或异常

原生端需要保持方法名、参数键和返回类型一致,不能只保留方法名称而改变业务语义。

4.1 跨端架构与调用时序

同一 MethodChannel 双向传递协议。Dart 调用 setDropEnabled、startDrag 和 getDirectoryEntries;原生反向调用 onDragEnter、onDragMove、onDragLeave、onDrop。区域命中使用窗口坐标转换后的组件范围。

Flutter 页面

Dart 对外 API

MethodChannel drag_and_drop_flutter

ArkTS DragAndDropFlutterPlugin

UDMF 与 ArkUI DragController

方法结果

4.1.1 一次完整接收拖放的时序
系统拖放 ArkTS 插件 OHOS Dart 平台 Flutter 区域 系统拖放 ArkTS 插件 OHOS Dart 平台 Flutter 区域 buildDropArea 并注册组件范围 DragEnter / DragMove onDragEnter / onDragMove 与坐标、MIME canDrop 与区域回调 setDropEnabled Drop 与 UDMF 数据 onDrop 完整内容 DragData 长按,传入 dragData 与指针 ID startDrag 创建 DragAction 并启动 ENDED 释放预览与拖拽引用

4.2 数据模型:MIME 元数据与 DragData

模型字段或用途边界
DataTransferItemMetadatatype、isFile进入区域阶段只有元数据
DataTransferItemtype、data 或 file放下后才处理完整内容
FileEntry / DirectoryEntry文件或目录对象目录枚举受 URI 权限约束
DragData.typecopy、move、linkOHOS 出站无法强制操作类型
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 中设置版本。下面按第二节补全 compileSdkVersiontargetSdkVersion,请合并到现有工程;其中 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.mdOHOS 新增能力、适配版本、兼容限制与测试范围
LICENSE / NOTICE保留上游许可证;NOTICE 按许可证和原项目要求保留或补充
example/README.md依赖方式、运行目录、签名、操作步骤与效果图;覆盖拖入、拒绝放下、主动拖出、文件读取和目录枚举
pubspec.yamlohos/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_flutterpath 配置替换为下面的 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.lockdrag_and_drop_flutter 的来源为 git,并核对 urlrefresolved-ref。同时检查没有 dependency_overridespubspec_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 模块配置自动签名:

  1. 用 DevEco Studio 打开 example/ohos,不是仓库根目录;
  2. 等待工程 Sync 成功,确认 Project 视图中存在 entry 模块;
  3. 打开 File > Project Structure > Signing Configs
  4. default product 选择或生成签名;
  5. 确认设备、应用包名、证书和 Profile 匹配;
  6. 再回到终端执行 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 在设备上测试原生拖入与拖出

  1. 运行完整 Demo,确认 Drag and Drop 页面有 Drag out 和 Drop in 两个区域。
  2. 从支持系统拖放的来源应用拖入纯文本或链接,检查 Drop in 的内容与类型。
  3. 拖入 canDrop 不接受的类型,检查没有错误触发 onDrop。
  4. 拖入文件并读取内容,拖入目录并调用 getEntries,核对授权不足时的行为。
  5. 长按 Long press and drag this link,拖到支持链接的目标应用。
  6. 拖离区域、反复拖入拖出、退出重进,确认回调与预览释放正常。

现有 OpenHarmony 说明记录了 API 24 设备的安装与启动,并明确跨应用拖入、拖出需要配合验证。本文不将该记录扩展为全部拖放功能已经真机通过。

8.6 鸿蒙设备运行效果

OHOS 实现提供原生拖入与拖出。以下为仓库完整 Demo 的三张真机运行截图,按实际状态记录。

drag_and_drop_flutter 鸿蒙真机:拖放区域 drag_and_drop_flutter 鸿蒙真机:释放后的实际结果 drag_and_drop_flutter 鸿蒙真机:主动拖拽与悬停

真机运行图(从左到右):拖放区域/释放后的实际结果/主动拖拽与悬停。采集于 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 的版本是否匹配。

处理顺序:

  1. 在 DevEco Studio SDK Manager 中确认 API 26 组件已经下载完整;
  2. 检查 Flutter 和 DevEco Studio 使用的 SDK 路径是否一致;
  3. 避免误用 /Applications/DevEco-Studio.app/Contents/sdk 之类的不完整目录;
  4. 确认 SDK 根目录下存在 toolchainsetsjsnativepreviewer
  5. 执行 flutter config --ohos-sdk <正确路径>
  6. 重新执行 flutter doctor -v 和 DevEco Studio Sync。
为什么连接 API 24 手机仍然会报这个错误?

因为 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.json5modules 是否包含 ./entry。同步失败时,Project Structure 无法正确解析模块,签名界面也可能不显示 entry。

9.3 无法手动签名

签名配置依附于可构建的应用模块和 product。只有 HAR 插件模块、工程 Sync 失败,或者打开了错误目录时,DevEco Studio 都可能无法提供 entry 签名入口。

建议先确认:

  • 打开的是 example/ohos
  • SDK 组件完整并且 Sync 成功;
  • entry 的模块类型为 entry
  • default product 和 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.yamlohos/index.etsoh-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.lockresolved-ref。原生代码变动后停止应用并重新构建运行,不能只做 Dart 热重载。

9.11 长按报 INVALID_POINTER_ID,或预览不像组件

当前实现从 PointerDownEvent.embedderId 获取实际指针并校验 0–9;不要沿用旧文档的固定 0 描述。预览由插件生成,暂不截取 child 外观,copy/move/link 最终也由系统和目标端决定。


相关链接

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐