Flutter 剪贴板插件 click_to_copy 鸿蒙适配实战:源码改造、工程配置、权限报错解决与真机调试全流程教程
开发工具: 华为云码道
本文配套仓库: 上游 spiderprogrammer/click_to_copy;鸿蒙适配改动位于本地仓库的
ohos/与example/ohos/。
鸿蒙适配后仓库:https://atomgit.com/oh-flutter/click_to_copy
click_to_copy 把系统剪贴板的读写封装成两个静态方法:ClickToCopy.copy(text) 写入剪贴板,ClickToCopy.paste() 读取剪贴板文本。Dart 层不维护自己的通道,而是直接调用 Flutter 框架的 Clipboard 服务,真正的平台读写由 Flutter Engine 完成。本文以 click_to_copy 0.0.1 为例,介绍源码准备、OHOS 工程配置、ArkTS 实现和 example 真机运行。
插件目前支持 ohos 平台,上游源码位于 GitHub 仓库。文中的代码以上游 master 分支提交 17ea4c77503af1c39c790895ab48abff3d2c985b 为适配基线。

OHOS 适配https://atomgit.com/oh-flutter/click_to_copy改动当前在 main 分支工作区,发布 TAG 为 ohos-0.0.1。

| Example 启动点击 Click to Copy | 点击 Click to Copy | 首次读取剪贴板,点击 Click to Paste | 点击 Click to Paste(需要授权) |
|---|---|---|---|
| 文本进入系统剪贴板,提示需要在输入框中输入需要拷贝的文案 | 文本进入系统剪贴板,可在其他应用粘贴 | 受 READ_PASTEBOARD 授权管控,未授权时读取不到内容 | 粘贴剪贴板纯文本 |
以下是操作的视屏,可以参考一下:
一、插件简介与适配目标
剪贴板是跨应用协作的基础能力。click_to_copy 把“点击复制、点击粘贴”封装成两个静态方法:ClickToCopy.copy(text) 把文本写入系统剪贴板,ClickToCopy.paste() 读取系统剪贴板的纯文本。Dart 层没有自建通道,而是直接调用 Flutter 框架的 Clipboard 服务,真正的平台读写由 Flutter Engine 的内建剪贴板实现完成。
例如,分享页的“复制链接”按钮调用 copy 后提示“已复制”;表单页的“粘贴”按钮调用 paste,把最近复制的文本填入输入框。
正因如此,这个插件的 OHOS 适配工作量很小:在 pubspec.yaml 声明 ohos 平台并生成 HAR 模块后,Engine 的剪贴板能力直接接管读写;原生侧只需保留与 Android/iOS 一致的通道契约——注册 click_to_copy 通道,并让 getPlatformVersion 返回与两端格式一致的系统版本字符串。
二、环境准备
环境搭建参考社区文档: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 分支 | ohos-0.0.1 | CPF-Flutter 对应开发分支 |
| Dart SDK | 3.12.2 | Dart 语言与包管理环境 |
| HarmonyOS 开发套件 | 26.0.0(API 26) | 开发套件版本及对应的 API 级别 |
compatibleSdkVersion | 5.1.0(18) | 当前工程声明的最低兼容版本 |
| 插件版本 | 0.0.1 | pubspec.yaml 中的包版本 |
| 原生语言 | ArkTS | HarmonyOS 插件实现 |
| 插件产物 | HAR | 被应用 entry 模块依赖 |
2.1 开发套件版本与工程中的 SDK 版本配置
26.0.0(API 26) 涉及 HarmonyOS 开发套件版本(API 版本)及其底座 OpenHarmony 的版本号体系。在本文工程中,相关版本的含义与配置方式如下:
26.0.0(API 26)表示本机 DevEco Studio 安装的开发套件为26.0.0,对应 API 26,Flutter 工具链构建时使用该 SDK;- 本工程的
example/ohos/build-profile.json5没有显式声明compileSdkVersion和targetSdkVersion,构建时按开发套件默认的 API 26 编译; 5.1.0(18)是本文工程中compatibleSdkVersion的属性值,声明最低兼容 API 18。
对应的 product 配置为:
{
"name": "default",
"signingConfig": "default",
"compatibleSdkVersion": "5.1.0(18)",
"runtimeOS": "HarmonyOS"
}

这组配置最低兼容 API 18。粘贴时读取系统剪贴板用到的 ohos.permission.READ_PASTEBOARD 从 API 12 起生效,授权和读取行为还取决于设备系统版本;本文真机为 OpenHarmony 6.1.1.120(API 24),满足要求。
三、从源码仓库开始准备适配工程
3.1 将上游源码同步到 AtomGit
适配已有第三方插件时,先从 Pub 包信息或项目 README 确认上游地址、包名、版本和许可证。同步仓库时保留源码、许可证和提交历史,方便后续跟踪上游更新。
在 AtomGit 网页的新建仓库流程中,找到从已有仓库导入的入口,填写上游 Git 地址,选择自己有写权限的组织或个人空间,设置目标仓库名称后执行导入。完成后检查目标分支、pubspec.yaml、LICENSE 和提交历史是否完整,并记录适配起点的提交号。如果上游本身托管在 AtomGit,也可以通过 Fork 获得自己的工作仓库。
click_to_copy 的上游位于 GitHub,采用 BSD 三条款许可证。可以按上文流程把上游仓库导入 AtomGit 或其他 Git 托管平台,获得自己有写权限的工作仓库;本文直接从 GitHub 地址拉取代码。
3.2 将代码拉取到宿主机
在安装了 Flutter OH 和 DevEco Studio 的开发电脑上打开终端,进入准备存放项目的目录,执行:
git clone https://github.com/spiderprogrammer/click_to_copy.git
cd click_to_copy
pwd
ls
git remote -v
git status --short --branch
git rev-parse HEAD

git clone 会创建 click_to_copy/ 目录;cd 后的位置就是下文所说的插件仓库根目录,这里应能看到 pubspec.yaml、lib/ 和 example/。本例的仓库名与 Dart 包名相同,都是 click_to_copy。
需要使用与本文相同的代码版本时,在没有未提交修改的仓库中切换到以下提交:
git switch --detach 17ea4c77503af1c39c790895ab48abff3d2c985b
适配其他插件时,使用该插件对应版本的 tag 或 commit 作为分支起点。

图 1:在宿主机终端输入仓库拉取命令。
3.3 在仓库根目录创建适配分支
接着在 click_to_copy/ 根目录创建分支。分支名统一使用 feat/ohos_库名称_版本号,库名称取 pubspec.yaml 的 name,版本号取此次适配的基线版本。本例为:
git switch -c feat/ohos_click_to_copy_0.0.1
git branch --show-current
如果该分支已存在,使用 git switch feat/ohos_click_to_copy_0.0.1 切换即可。上游仓库的 master 分支是当前主线,没有打过 tag,本次适配以 master 的提交 17ea4c7 为基线。

图 2:在 click_to_copy 仓库根目录输入适配分支创建命令。
3.4 自动补全 OHOS 适配结构
分支创建后,仍在同一个插件根目录执行结构补全。以下命令适用于尚无 ohos/ 目录的既有 Flutter 平台插件:
flutter create --template=plugin --platforms=ohos --project-name click_to_copy .
git status --short
git diff -- pubspec.yaml .gitignore
--template=plugin指定插件模板。--platforms=ohos指定需要补全的平台。--project-name click_to_copy使用 Dart 包名,与pubspec.yaml中的name保持一致。- 最后的
.表示在当前插件目录补全工程,不是另建一层click_to_copy/。
本例执行后 git status --short 的输出为:
M .gitignore
M example/ios/Flutter/Debug.xcconfig
M example/ios/Flutter/Release.xcconfig
M example/pubspec.lock
M pubspec.yaml
?? example/ios/Podfile
?? example/ohos/
?? ohos/
pubspec.yaml 新增了 ohos: pluginClass: ClickToCopyPlugin 两行;.gitignore 追加了 ohos/ 构建产物和本机缓存的忽略规则;ohos/ 与 example/ohos/ 是新生成的 HAR 脚手架和宿主工程;example/ios/Podfile 与两份 xcconfig 的变化来自模板对 iOS 示例工程的同步更新。生成后通过 diff 检查变化,保留已有 API、其他平台注册项及依赖配置。不同 Flutter OH 版本生成的模板可能略有差异。
如果生成后 example/ohos/ 仍不存在,进入已有示例应用补全平台:
cd example
flutter create --platforms=ohos .
cd ..
配套仓库已经包含 ohos/ 和 example/ohos/,直接运行示例时可以跳过结构补全。新建插件则使用 flutter create --org com.nutpi --template=plugin --platforms=ohos click_to_copy;已有插件使用上面的 . 在当前目录补全。

图 3:在插件根目录输入 OHOS 结构补全命令。
3.5 适配后的项目目录
适配后的关键目录如下:
click_to_copy/
├── lib/
│ └── click_to_copy.dart
├── ohos/
│ ├── index.ets
│ ├── oh-package.json5
│ ├── build-profile.json5
│ ├── hvigorfile.ts
│ └── src/main/
│ ├── ets/components/plugin/ClickToCopyPlugin.ets
│ └── module.json5
├── example/
│ ├── lib/main.dart
│ └── ohos/entry/
├── test/
│ └── click_to_copy_test.dart
├── android/
├── ios/
└── pubspec.yaml
项目根目录如下,其中包含 ohos/、example/,以及上游的 README.md 和 CHANGELOG.md:

图 4:适配后的 click_to_copy 项目根目录。
| 文件 | 主要职责 |
|---|---|
lib/click_to_copy.dart | 提供 copy / paste 静态方法,委托框架 Clipboard 服务 |
ClickToCopyPlugin.ets | 注册 click_to_copy 通道,保持与 Android/iOS 一致的命令契约 |
插件 module.json5 | 声明 HAR 模块 |
示例 entry module.json5 | 声明宿主应用 Ability、设备类型和剪贴板读取权限 |
example/lib/main.dart | 上游 Demo:两个输入框加复制、粘贴按钮 |
四、Dart 接口与通道分析
OHOS 适配要分清两条通道的职责:复制粘贴走 Flutter 框架的系统通道 flutter/platform,由 Engine 内建实现承担;插件自有通道 click_to_copy 只保留命令契约。先阅读 lib/click_to_copy.dart 和 Android/iOS 的原生实现,再在 ohos/src/main/ets/components/plugin/ 中实现对应的原生类。适配其他已有平台的插件时,也应保留其公开接口和其他平台实现。
本例的对应关系如下:
| Dart 入口或模型 | 通道协议 | OHOS 实现 | 应保持的行为 |
|---|---|---|---|
ClickToCopy.copy(text) | 框架系统通道 flutter/platform | Flutter Engine 内建剪贴板实现 | 文本写入系统剪贴板 |
ClickToCopy.paste() | 框架系统通道 flutter/platform | 同上 | 返回剪贴板纯文本或空字符串 |
getPlatformVersion(模板探活) | 插件通道 click_to_copy | ClickToCopyPlugin.ets 返回 'OpenHarmony ' + osFullName | 与 Android/iOS 返回格式一致 |
通道名和命令名属于跨语言协议。任何一端拼写不一致,都会让探活命令失效或出现 MissingPluginException。
4.1 跨端架构与调用时序
Flutter 侧和 HarmonyOS 侧之间有两个参与者,但它们互不交叉:
- 框架系统通道
flutter/platform:Clipboard.setData/Clipboard.getData的消息由 Engine 的 OHOS 平台层翻译成系统剪贴板调用,承担真正的复制粘贴; - 插件自有通道
click_to_copy:Dart 侧当前业务代码并不调用它,原生侧注册后只响应getPlatformVersion,用于保持与 Android/iOS 一致的插件契约。
插件通道只承担契约保留,复制粘贴完全依赖框架系统通道。
4.1.1 一次完整复制与粘贴的时序
4.2 工具类模型:lib/click_to_copy.dart
ClickToCopy 是纯静态工具类,没有状态和生命周期:
class ClickToCopy {
// Copy data to Clipboard
static Future<void> copy(String text) async {
if (text.isNotEmpty) {
await Clipboard.setData(ClipboardData(text: text));
debugPrint('ClickToCopy: $text');
return;
} else {
debugPrint('ClickToCopy: No text to copy on Clipboard');
//throw ('No text to copy on Clipboard');
}
}
// Paste data copied from Clipboard
static Future<String> paste() async {
ClipboardData? data = await Clipboard.getData('text/plain');
debugPrint('ClickToCopy: ${data?.text?.toString() ?? ''}');
return data?.text?.toString() ?? '';
}
}
| 成员 | 签名 | 行为 |
|---|---|---|
copy | static Future<void> copy(String text) | 非空文本写入剪贴板;空文本只打印日志,不抛异常 |
paste | static Future<String> paste() | 读取纯文本;剪贴板为空时返回空字符串 |
两个边界处理都值得注意:copy 对空文本静默跳过(源码里 throw 被注释掉了),调用方不会收到失败信号;paste 用 ?? '' 兜底,剪贴板为空与读取失败都表现为空字符串。两者都通过 debugPrint 输出调试日志,真机排查时可以在控制台看到 ClickToCopy: 前缀。
4.3 公开 API 与平台接口
公开 API 只有两个静态方法,库没有单独的平台接口层(没有 platform_interface / method_channel 文件):
static Future<void> copy(String text) async { ... }
static Future<String> paste() async { ... }
- 两者都是
async,内部await框架Clipboard服务后返回; - 调用时机完全由业务决定:
copy在用户点击复制类按钮时调用一次;paste在需要读取时调用,立即返回当前剪贴板内容; - 没有
Stream、回调或需要取消的订阅,重复调用互不影响。
pubspec.yaml 中的三端 pluginClass: ClickToCopyPlugin 只用于原生插件注册,与这两个方法的调用路径无关。
4.4 Dart 通道协议分析
4.4.1 通道名称必须两端完全一致
插件自有通道的名称三端完全一致,都是 click_to_copy:
// Android
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "click_to_copy")
// iOS
let channel = FlutterMethodChannel(name: "click_to_copy", binaryMessenger: registrar.messenger())
// OHOS
this.channel = new MethodChannel(binding.getBinaryMessenger(), "click_to_copy");
当前 Dart 业务代码并不调用这条通道(复制粘贴走框架服务),但测试模板和未来扩展都依赖这一契约,OHOS 侧注册时必须与两端拼写一致。
4.4.2 命令处理与契约保留
原生侧的命令处理保持与 Android/iOS 相同的语义——只实现 getPlatformVersion,其余返回 notImplemented:
// Android 返回值,作为对照
result.success("Android ${android.os.Build.VERSION.RELEASE}")
// OHOS
onMethodCall(call: MethodCall, result: MethodResult): void {
try {
if (call.method == "getPlatformVersion") {
result.success("OpenHarmony " + this.getPlatformVersion());
} else {
result.notImplemented();
}
} catch (e) {
result.error("ClickToCopyError", "Failed to handle method: " + call.method
+ ", error: " + (e as Error).message, e);
}
}
版本字符串带平台前缀,与 Android 的 "Android "、iOS 的 "iOS " 格式对齐。每个方法调用都被 try/catch 包裹:原生侧异常被转换成 result.error 回到 Dart,而不是让插件崩溃。
4.4.3 解绑与清理
插件没有需要取消的订阅。Engine 解绑时,onDetachedFromEngine 清理通道处理器:
onDetachedFromEngine(binding: FlutterPluginBinding): void {
if (this.channel != null) {
this.channel.setMethodCallHandler(null);
this.channel = null;
}
}
这一清理覆盖业务未主动释放的情况;click_to_copy 通道上也没有需要持久保存的原生状态。
五、补全 OHOS 原生实现与工程配置
5.1 在 ClickToCopyPlugin.ets 中实现原生能力
业务仍使用 ClickToCopy.copy / ClickToCopy.paste,复制粘贴由 Engine 的内建剪贴板实现承担,不需要插件写任何剪贴板代码。需要补全的是 ClickToCopyPlugin.ets 中的通道注册与命令契约:注册 click_to_copy 通道,让 getPlatformVersion 返回与 Android/iOS 格式一致的系统版本字符串,并把所有调用置于异常保护之下。
ClickToCopyPlugin 实现 FlutterPlugin 和 MethodCallHandler,没有平台视图,也不需要 AbilityAware。
原生插件位于(本库只有一个原生文件):
ohos/src/main/ets/components/plugin/ClickToCopyPlugin.ets
5.1.1 引入 Flutter 和 HarmonyOS 能力
import {
FlutterPlugin,
FlutterPluginBinding,
MethodCall,
MethodCallHandler,
MethodChannel,
MethodResult,
} from '@ohos/flutter_ohos';
import deviceInfo from '@ohos.deviceInfo';
其中:
FlutterPlugin负责接入 Flutter Engine 生命周期;MethodChannel、MethodCall、MethodCallHandler、MethodResult组成命令处理的完整链路;deviceInfo提供osFullName系统版本名,用于拼装getPlatformVersion的返回值。
与剪贴板相关的导入一个都没有:复制粘贴不走插件,插件也不直接依赖 @ohos.pasteboard。
5.1.2 连接 Flutter Engine
getUniqueClassName(): string {
return "ClickToCopyPlugin";
}
onAttachedToEngine(binding: FlutterPluginBinding): void {
this.channel = new MethodChannel(binding.getBinaryMessenger(), "click_to_copy");
this.channel.setMethodCallHandler(this);
}
Engine 启动时创建通道并注册处理器,通道名与 Android/iOS 一致。getUniqueClassName 返回类名,供引擎侧的插件管理使用。
5.1.3 保持版本命令契约
版本命令的契约是“平台名 + 系统版本”:
private getPlatformVersion(): string {
// osFullName is verified against
// <SDK>/ets/api/@ohos.deviceInfo.d.ts (const osFullName: string).
return deviceInfo.osFullName;
}
三端对照如下:
| 平台 | 返回值 |
|---|---|
| Android | "Android " + Build.VERSION.RELEASE |
| iOS | "iOS " + UIDevice.current.systemVersion |
| OHOS | "OpenHarmony " + deviceInfo.osFullName |
osFullName 的类型在 SDK 的 @ohos.deviceInfo.d.ts 中声明为常量字符串;代码注释里保留了这一核对记录,说明取值依据来自本机 SDK 的接口定义。
5.1.4 剪贴板能力的真实承担者
剪贴板能力的真实承担者不是插件。Dart 侧 Clipboard.setData / Clipboard.getData 会通过框架的 SystemChannels.platform(即 flutter/platform 系统通道)发送消息,由 Flutter Engine 的 OHOS 平台层翻译成系统剪贴板调用:
// framework/services.dart(Flutter 框架源码,示意)
await SystemChannels.platform.invokeMethod('Clipboard.setData', ...);
final ClipboardData? data = await SystemChannels.platform.invokeMethod('Clipboard.getData', ...);
这意味着:
- 插件原生代码不 import
@ohos.pasteboard,也不处理剪贴板消息; - 只要 OHOS Flutter Engine 的剪贴板实现可用,
copy/paste就能工作; - 需要额外准备的只有宿主应用的
READ_PASTEBOARD权限声明,见 5.2。
验证这条链路是否通畅,比给插件堆剪贴板代码更重要:适配完成后在真机上执行 8.5 的复制粘贴步骤即可确认。
5.1.5 处理命令与异常保护
onMethodCall 的完整实现把命令分发包进 try/catch:
onMethodCall(call: MethodCall, result: MethodResult): void {
try {
if (call.method == "getPlatformVersion") {
result.success("OpenHarmony " + this.getPlatformVersion());
} else {
result.notImplemented();
}
} catch (e) {
result.error("ClickToCopyError", "Failed to handle method: " + call.method
+ ", error: " + (e as Error).message, e);
}
}
- 未知命令一律
result.notImplemented(),与上游 Android/iOS 行为一致,Dart 侧会收到MissingPluginException而不是原生崩溃; - 异常统一转换成错误码
"ClickToCopyError"的result.error,错误信息包含命令名和异常消息,方便 Dart 侧定位。
5.1.6 Engine 解绑时释放资源
onDetachedFromEngine(binding: FlutterPluginBinding): void {
if (this.channel != null) {
this.channel.setMethodCallHandler(null);
this.channel = null;
}
}
Flutter Engine 销毁时清理通道 Handler 并释放引用。插件没有其他原生资源(无视图、无监听器、无权限句柄),解绑即完成全部清理。
5.2 声明插件和宿主权限
写入系统剪贴板不需要权限;读取剪贴板从 API 12 起受 ohos.permission.READ_PASTEBOARD 保护,属于 user_grant 权限。本例插件代码不包含运行时申请逻辑,授权由系统在应用读取剪贴板时管控,宿主应用负责静态声明。当前宿主工程声明了以下权限:
ohos.permission.INTERNET
ohos.permission.READ_PASTEBOARD
5.2.1 插件 HAR 的权限
插件的 ohos/src/main/module.json5 只声明 HAR 模块信息,不带 requestPermissions:
{
"module": {
"name": "click_to_copy",
"type": "har",
"deviceTypes": ["default", "tablet"]
}
}
权限统一由宿主应用声明,HAR 保持无权限依赖,接入方可以按自己的场景决定是否授权。
5.2.2 应用 entry 的权限
最终安装的是宿主应用。本例需要修改仓库根目录下的 example/ohos/entry/src/main/module.json5,在现有 module 配置中合并以下权限和使用场景,保留原有 Ability 等配置:
{
"module": {
"requestPermissions": [
{"name": "ohos.permission.INTERNET"},
{
"name": "ohos.permission.READ_PASTEBOARD",
"reason": "$string:read_pasteboard_reason",
"usedScene": {
"abilities": ["EntryAbility"],
"when": "inuse"
}
}
]
}
}
权限原因资源合并到 example/ohos/entry/src/main/resources/base/element/string.json 的现有 string 数组中:
{
"string": [
{
"name": "read_pasteboard_reason",
"value": "Read the clipboard content when you tap Paste"
}
]
}
本工程的原因文案是英文,正式发布时可按目标用户本地化。静态声明只是第一步:未获得授权时,读取剪贴板会被系统拦截,paste() 表现为返回空内容,需要在真机上确认目标设备的授权行为。
5.3 注册并导出插件
pubspec.yaml 通过以下配置声明 OHOS 插件类:
flutter:
plugin:
platforms:
ohos:
pluginClass: ClickToCopyPlugin
插件的 ohos/index.ets 需要导出实现:
import ClickToCopyPlugin from './src/main/ets/components/plugin/ClickToCopyPlugin';
export default ClickToCopyPlugin;
执行 flutter pub get 和构建后,Flutter 工具会为应用生成插件注册代码,GeneratedPluginRegistrant.ets 中会出现:
import { FlutterEngine, Log } from '@ohos/flutter_ohos';
import ClickToCopyPlugin from 'click_to_copy';
export class GeneratedPluginRegistrant {
static registerWith(flutterEngine: FlutterEngine) {
try {
flutterEngine.getPlugins()?.add(new ClickToCopyPlugin());
} catch (e) {
Log.e(TAG, "Tried to register plugins with FlutterEngine failed.");
}
}
}
通常不应手工编辑该文件,因为下次构建可能覆盖它。注册异常的排查步骤见第九节 MissingPluginException。
5.4 检查 example 的 OHOS 应用结构
本例的 example/ohos/build-profile.json5 应在 products 中设置版本。下面是需核对的配置片段,请合并到现有工程;其中 signingConfig: "default" 需要与本机配置的签名名称一致,签名材料保留在本地:
{
"app": {
"products": [
{
"name": "default",
"signingConfig": "default",
"compatibleSdkVersion": "5.1.0(18)",
"runtimeOS": "HarmonyOS"
}
]
},
"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 | 简介、安装方式、版本对应关系、环境约束、权限、接口表、示例、已验证范围和遗留问题 |
README.OpenHarmony.md | 与中文说明对应的英文文档 |
CHANGELOG.OpenHarmony.md | OHOS 新增能力、适配版本、兼容限制与测试范围 |
LICENSE / NOTICE | 保留上游许可证;NOTICE 按许可证和原项目要求保留或补充 |
example/README.md | 依赖方式、运行目录、签名、操作步骤与效果图;覆盖复制与粘贴操作 |
pubspec.yaml、ohos/oh-package.json5 | 核对包名、版本、插件注册、仓库地址、许可证和依赖 |
.gitignore | 忽略构建缓存及本机签名材料,不漏提交必要源码和配置 |
本仓库当前只完成了代码适配:README.md 仍是上游的使用说明,README.OpenHarmony_CN.md、README.OpenHarmony.md、CHANGELOG.OpenHarmony.md 和 example/README.md 尚未补齐,也没有归档真机截图,属于本次适配的待交付项。
两处信息需要在提交前修正:ohos/oh-package.json5 的 license 字段仍是脚手架默认值 Apache-2.0,与上游 LICENSE(BSD 三条款)不一致,应改为一致的许可证声明;pubspec.yaml 的 environment.sdk 上限仍是上游的 <3.0.0,影响范围见 9.11。
6.2 提交前检查
提交前先完成第八节的插件、example 和真机测试,再从根目录检查改动:
git branch --show-current
git diff --check
git status --short
git diff --stat
git diff
检查 diff 中的接口、平台注册和依赖变化,移除本机路径及签名信息,并使文档中的版本和分支与提交内容一致。
6.3 提交并推送适配分支
文档和代码整理完成后,在根目录暂存并提交。文件名按项目实际情况调整:
git add ohos example/ohos pubspec.yaml .gitignore
git add example/pubspec.lock
git add example/ios/Podfile
git add example/ios/Flutter/Debug.xcconfig example/ios/Flutter/Release.xcconfig
git diff --cached --check
git diff --cached --stat
git diff --cached
git commit -m "feat: add OHOS support for click_to_copy 0.0.1"
git remote -v
git branch --show-current
git push -u origin feat/ohos_click_to_copy_0.0.1
DevEco 可能向 example/ohos/build-profile.json5 写入本机签名配置,需要在提交前从暂存内容中移除。推送时,origin 应指向自己有写权限的仓库,当前分支为 feat/ohos_click_to_copy_0.0.1;上游仓库在 GitHub,无权限直接推送时先推送到自己的镜像。
推送后在托管平台发起合并请求,说明上游来源和版本、OHOS 实现范围、依赖及权限、测试环境、操作结果、已知限制,并附 Demo 运行说明或运行图。目标分支和评审流程以接收仓库要求为准。
七、使用根目录 example 演示接入
仓库自带 example/,可以直接用来调试插件和体验复制与粘贴流程。
7.1 本地适配时使用路径依赖
当前 example/pubspec.yaml 的依赖是:
dependencies:
flutter:
sdk: flutter
click_to_copy:
path: ../
../ 相对于 example/pubspec.yaml 指向插件根目录,修改根目录插件后可直接联调。
7.2 通过 Git 依赖引入插件
业务应用通过 Git 引入时,将 click_to_copy 的 path 配置替换为下面的 Git 依赖。上游 GitHub 仓库尚未包含 OHOS 适配,先按第六节把自己的适配分支推送到有写权限的仓库,再固定到该分支:
dependencies:
flutter:
sdk: flutter
click_to_copy:
git:
url: https://atomgit.com/{your-org}/click_to_copy.git
ref: feat/ohos_click_to_copy_0.0.1
使用自己的适配版本时,将 url 改为对应仓库。正式发布后可固定到 tag 或 commit。
从插件根目录执行:
cd example
flutter pub get
flutter pub deps
检查 example/pubspec.lock 中 click_to_copy 的来源为 git,并核对 url、ref 和 resolved-ref。同时检查没有 dependency_overrides 或 pubspec_overrides.yaml 将其覆盖回本地依赖,确认应用使用的是 Git 依赖。
7.3 调用接口实现复制与粘贴
下面的页面把复制和粘贴放进同一个输入框,可用于 example/lib/main.dart。输入文本后点击复制,清空输入框,再点击粘贴,文本会回来:
import 'package:click_to_copy/click_to_copy.dart';
import 'package:flutter/material.dart';
void main() {
runApp(const MaterialApp(home: CopyPastePage()));
}
class CopyPastePage extends StatefulWidget {
const CopyPastePage({super.key});
State<CopyPastePage> createState() => _CopyPastePageState();
}
class _CopyPastePageState extends State<CopyPastePage> {
final TextEditingController _controller = TextEditingController();
void dispose() {
_controller.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Click to Copy')),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
TextField(
controller: _controller,
decoration: const InputDecoration(hintText: 'Type...'),
),
TextButton(
onPressed: () async {
if (_controller.text.trim().isNotEmpty) {
await ClickToCopy.copy(_controller.text.trim());
}
},
child: const Text('Click to Copy'),
),
TextButton(
onPressed: () async {
final value = await ClickToCopy.paste();
setState(() {
_controller.text = value.trim();
});
},
child: const Text('Click to Paste'),
),
],
),
),
);
}
}
仓库中的完整 Demo 是上游原样代码:两个输入框加两个按钮——第一个框输入文本并校验非空后复制,第二个只读框点击粘贴后显示剪贴板内容,粘贴结果为空时清空显示。
7.4 页面退出时的资源处理
ClickToCopy 是无状态静态方法,没有需要取消的订阅;页面 dispose 中只需释放自己的 TextEditingController 后调用 super.dispose()。
上游 Demo 的 dispose 顺序值得注意:它把 super.dispose() 放在最前面、之后才释放控制器。Flutter 的规范是先释放自己的资源、最后调用 super.dispose(),接入时建议按本节开头的方式调整,避免在已标记 disposed 的状态上继续操作。
八、验证、构建与鸿蒙设备运行效果
8.1 分别验证插件与 example
从插件仓库根目录执行:
flutter pub get
flutter analyze
flutter test
cd example
flutter pub get
flutter analyze
本仓库当前的 flutter analyze 报 4 项:test/click_to_copy_test.dart 引用了当前 API 中不存在的 ClickToCopy.platformVersion(error),同文件两处使用了已废弃的 setMockMethodCallHandler(info),以及 example/lib/main.dart 一处可能正常结束的空返回路径(warning)。
flutter test 会在编译阶段失败,报错 Member not found: 'platformVersion':测试文件来自插件模板,上游早已把版本接口从公开 API 中移除。处理方式是删除或改写该测试(例如改为验证 paste() 的空剪贴板兜底),不属于 OHOS 适配本身的缺陷。
Dart 测试只能覆盖工具类的分支逻辑,系统剪贴板的实际读写、权限行为和跨应用粘贴还需要在鸿蒙设备上验证。
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。真机安装应选择与当前设备匹配的已签名产物。构建失败时按第九节的检查项排查签名与 SDK 配置后重试。
8.5 在设备上测试复制与粘贴
- 打开应用,确认页面显示 “Click to Copy” 标题、输入框和复制、粘贴按钮(上游 Demo 原样);
- 在第一个输入框输入文本,点击 “Click to Copy”,确认控制台输出
ClickToCopy: <文本>; - 清空输入框,点击 “Click to Paste”,确认文本回到输入框(完整 Demo 中显示在第二个只读框);
- 打开系统其他应用(如备忘录)长按粘贴,确认写入的是步骤 2 复制的内容;
- 剪贴板为空时点击 “Click to Paste”,确认返回空字符串、界面清空且不崩溃;
- 输入框为空时直接点击 “Click to Copy”,确认只打印
No text to copy on Clipboard日志、无异常; - 多次复制不同文本后粘贴,确认每次读取的都是最新内容。
首次粘贴读取受 READ_PASTEBOARD 授权管控,未授权时读取不到内容;确认授权状态后再重复上述步骤。
8.6 鸿蒙设备运行效果
完成适配后,Flutter 应用在 OHOS 上的复制、粘贴行为与 Android/iOS 保持一致:copy 把文本写入系统剪贴板,paste 读取纯文本并返回,跨应用粘贴可用。
本仓库当前未归档真机运行截图,运行效果以上文 8.5 的操作步骤描述为准;补齐截图后,可在此处并列展示输入页面、复制操作和粘贴结果。
| Example 启动点击 Click to Copy | 点击 Click to Copy | 首次读取剪贴板,点击 Click to Paste | 点击 Click to Paste(需要授权) |
|---|---|---|---|
| 文本进入系统剪贴板,提示需要在输入框中输入需要拷贝的文案 | 文本进入系统剪贴板,可在其他应用粘贴 | 受 READ_PASTEBOARD 授权管控,未授权时读取不到内容 | 粘贴剪贴板纯文本 |
以下是操作的视屏,可以参考一下:
剪贴板能力依赖设备的剪贴板服务和权限框架,由 Flutter Engine 的 OHOS 平台层承担。即使系统版本满足要求,不同型号也可能存在能力差异,需要在目标设备上测试。
九、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 打开:
click_to_copy/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 能安装但复制或粘贴不生效
按以下顺序检查:
- 复制侧:输入文本是否为空——
copy对空文本只打印日志,不写入剪贴板; - 粘贴侧:entry 是否声明
READ_PASTEBOARD权限及reason资源; - 系统设置中确认应用是否已获得读取剪贴板授权;
- 剪贴板中是否为纯文本内容;
- 控制台是否出现
ClickToCopy:日志(copy 和 paste 各有一条 debugPrint); - 是否误判了故障位置——真正承担读写的是框架系统通道
flutter/platform,插件通道click_to_copy只用于探活命令。
9.5 MissingPluginException
这通常表示 Dart 通道找不到已注册的原生插件。新增原生插件后需要重新构建应用。从仓库根目录执行:
cd example
flutter clean
flutter pub get
flutter run -d <device-id>
如果仍然出现,检查自动生成的插件注册文件中是否包含 ClickToCopyPlugin,同时核对 pubspec.yaml、ohos/index.ets 和 oh-package.json5。
9.6 重复调用没有累积效果
ClickToCopy 的两个方法都是无状态的一次性调用,没有可以取消的订阅,也没有累积的事件:
- 重复
copy同一文本:多次写入同一内容,剪贴板仍只有一条记录; copy空文本:静默跳过,只打印No text to copy on Clipboard,调用方不会收到失败信号;- 重复
paste:每次读取当前剪贴板并返回,上一次的返回值不会被记住,Demo 的第二个输入框每次都被最新结果覆盖。
如果业务需要“粘贴历史”或“复制成功回调”,需要自己保存每次调用的结果;插件层面没有去重、缓存或事件通知。
9.7 编译成功但安装失败
常见原因包括:
- HAP 未签名或使用了错误的 Profile;
- 设备未加入调试设备列表;
- 包名与签名 Profile 不匹配;
- 安装包的
compatibleSdkVersion高于设备 API; - 手机上已经安装了使用不同证书签名的同包名应用。
根据安装错误码区分签名、版本和包名冲突,再处理对应配置。
9.8 flutter create 不认识 ohos,或包名不合法
先执行 flutter --version,确认使用的是 OHOS 版工具链,环境配置回到第二节的社区链接核对。包名报错时,确认当前目录包含目标插件的 pubspec.yaml,并显式传入 --project-name click_to_copy。生成后检查 diff,再核对 ArkTS 实现与两端契约一致。
9.9 Git 依赖提示找不到分支或无权限
先检查 url 是否指向已包含 OHOS 适配的仓库:上游 GitHub 仓库的 master 分支尚不包含 ohos/ 目录,直接引用会构建失败。再确认 feat/ohos_click_to_copy_0.0.1 已按第六节提交推送。私有仓库还需在本机配置 Git 认证。
9.10 改了本地 ArkTS,Demo 为什么没变化
先检查 example/pubspec.yaml:Git 依赖读取远程提交,不会自动读取本地插件改动。本地联调切回 path: ../;测试远程版本则先提交推送,再更新依赖并核对 pubspec.lock 的 resolved-ref。原生代码变动后停止应用并重新构建运行,不能只做 Dart 热重载。
9.11 其他 Dart 3 环境下 pub get 报 SDK 约束不满足
仓库 pubspec.yaml 的 environment.sdk 仍是上游的 >=2.12.0 <3.0.0,这是 Dart 2 时代的约束。本套 OHOS 工具链(Dart 3.12.2)可以完成依赖解析,并按约束选择较旧的依赖版本;换到标准 Dart 3 工具链时,pub 会因当前 SDK 不满足 <3.0.0 上限而拒绝解析,报错类似 “The current Dart SDK version does not meet the constraint”。
处理方式:把上限放宽,例如改为 sdk: ">=2.12.0 <4.0.0",并同步修改 example/pubspec.yaml,然后执行 flutter pub get 重新生成两份 lock 文件。放宽后依赖会解析到新版本,提交前用 flutter pub deps 核对依赖树变化。
相关链接
更多推荐




所有评论(0)