开发工具: 华为云码道

本文配套仓库: 上游仓库 thorito/custom_clipboard
鸿蒙适配后仓库https://atomgit.com/oh-flutter/custom_clipboard

本文配套仓库:https://atomgit.com/oh-flutter/custom_clipboard(TAG:0.0.6-ohos-1.0.0-beta.1,分支:main),文中示例代码位于仓库 example/ 目录。

在这里插入图片描述

剪贴板是操作系统中最基础的跨应用数据通道。 在 HarmonyOS 中,系统通过 @ohos.pasteboard 模块向应用提供剪贴板服务,开发者可以写入文本、读取内容、清空数据。然而,Flutter 引擎内置的 Clipboard.setData(ClipboardData(text: '')) 只是向系统剪贴板写入一条空文本记录,并非真正意义上的"清空"——它仍然在系统剪贴板服务中留下一条空记录,某些场景下还会触发系统粘贴通知。对于安全敏感型应用(如密码管理器、银行 App、即时通讯工具),这种"伪清空"是不够的:用户期望的是剪贴板被彻底清除,没有任何残留。

本文介绍如何使用鸿蒙化适配后的 Flutter 三方库 custom_clipboard,在鸿蒙 App 内通过原生 pasteboard.getSystemPasteboard().clearData() 实现剪贴板的真正清空,同时提供跨平台一致的 getClipboard() 读取接口,并附上 OpenHarmony 6.1.1.120 真机的完整实测记录。


一、最终运行效果

应用启动后,在输入框中输入任意文本,点击 Copy Input to Clipboard 按钮将文本写入系统剪贴板,随后点击 Clear Clipboard 按钮调用原生清空接口,系统剪贴板服务日志确认数据已被彻底清除:

验证点结果
应用启动,Flutter 页面正常渲染通过
点击 Copy 按钮,文本成功写入系统剪贴板通过
系统输入法面板"来自剪贴板"气泡显示已复制文本通过
点击 Clear 按钮,原生 pasteboard.clearData() 执行成功通过
清空后 Clipboard.hasStrings() 返回 false,剪贴板无内容通过
点击 Get 按钮,getClipboard() 返回 null(受限权限,符合预期)符合预期(见 FAQ Q1)
全程无需申请任何权限即可完成写入与清空通过

Example 启动授权 复制后显示剪贴板内容 获取复制后显示剪贴板内容 清空后状态(需要授权)

Example 启动授权复制后显示剪贴板内容获取复制后显示剪贴板内容清空后状态(需要授权)
Example 启动READ_PASTEBOARD授权复制后显示剪贴板内容获取复制后显示剪贴板内容清空后状态(需要授权)

以下是操作的视屏,可以参考一下:

清空后状态(需要授权)

HarmonyOS 技术点:@ohos.pasteboard 模块

鸿蒙系统的剪贴板服务由 @ohos.pasteboard 统一管理。该模块提供了 getSystemPasteboard() 方法返回系统剪贴板实例 SystemPasteboard,其上挂载了 setData()getData()hasData()clearData() 等异步方法。与 Android 的 ClipboardManager 和 iOS 的 UIPasteboard 类似,鸿蒙的 pasteboard 同样是一个系统级单例服务,所有应用共享同一份剪贴板数据。区别在于:鸿蒙的 clearData() 会直接清除剪贴板中的全部记录,而非用空文本覆盖——这正是"真正清空"的语义所在。

图一:demo 应用在 OpenHarmony 真机启动,Flutter 页面完整渲染(API 24 / arm64)

图二:输入文本后点击 Copy,系统剪贴板服务日志确认写入成功,输入法"来自剪贴板"气泡同步展示

图三:点击 Clear Clipboard 后,原生插件执行 pasteboard.clearData(),剪贴板状态查询返回"无内容"

检查要点

  1. 剪贴板的写入与清空无需申请任何权限,属于系统能力的公开接口;
  2. 读取剪贴板内容受系统受限权限 ohos.permission.READ_PASTEBOARD 限制,第三方应用在调试签名下无法获得,引擎对 getClipboard() 返回 null(详见"七、工作原理"与 FAQ Q1);

在这里插入图片描述

  1. 查询剪贴板是否有内容(Clipboard.hasStrings())走引擎 hasData 路径,同样无需受限权限;
  2. 完整实测过程见"六、运行与验证"。

二、custom_clipboard 是什么

custom_clipboard 原库(GitHub thorito/custom_clipboard,版本 0.0.6)是一个清空系统剪贴板的 Flutter 插件。它的设计初衷很简单:不通过写入占位文本来"伪清空"剪贴板,而是调用各平台的原生剪贴板 API 进行真正意义上的清除。鸿蒙适配版在其基础上新增了 OpenHarmony / HarmonyOS 平台支持:通过 ArkTS 原生插件调用 pasteboard.getSystemPasteboard().clearData(),实现与 Android ClipboardManager.clearPrimaryClip()、iOS UIPasteboard.general.string = "" 语义一致的清空效果。

为什么不能直接用 Clipboard.setData(ClipboardData(text: ''))

Flutter 引擎内置的 Clipboard.setData 在鸿蒙端最终会调用 pasteboard.setData() 写入一条空文本记录。这条记录虽然内容为空,但仍然占据系统剪贴板的一个条目,可能触发系统的"已复制"通知,也可能被其他应用通过 hasData() 检测到剪贴板非空。对于安全敏感场景——比如用户刚刚复制了密码或验证码——这种残留是不可接受的。原生 clearData() 则直接清除剪贴板中的所有记录,不留任何痕迹。

几个对使用者友好的特点:

  1. 零权限清空:写入与清空剪贴板都是系统公开接口,不需要在 module.json5 中申请任何权限;
  2. 真正清空:不走写入占位文本的路径,而是调用系统原生 clearData() 直接清除剪贴板记录,与 Android / iOS 行为一致;
  3. 跨平台一套代码:Android、iOS、鸿蒙共用同一对 Dart 接口,Dart 层零改动,运行在哪端就按哪端的原生方式操作剪贴板;
  4. 联邦插件结构:平台实现通过 custom_clipboard_platform_interface 解耦,各端实现自动发现注册,无需手动注册。

接口说明:

名称描述类型参数类型返回值必填鸿蒙平台支持
getClipboard()读取剪贴板文本methodFuture<String?>是(受受限权限限制,见 FAQ Q1)
clearClipboard()清空剪贴板(引擎路径)methodFuture<void>
CustomClipboardPlatform.instance.clearClipboard()清空剪贴板(原生插件直调)methodFuture<void>

三个接口的区别clearClipboard() 是库级别的封装,在鸿蒙上走引擎内置的 Clipboard.setData('') 路径(写入空文本);而 CustomClipboardPlatform.instance.clearClipboard() 直接命中 ArkTS 原生插件的 pasteboard.clearData()(真正清空)。getClipboard() 则始终走引擎内置的 Clipboard.getData。在实际业务中,如需"真正清空",建议直接调用原生插件接口。


三、环境准备

本文所有实测均在以下环境完成:

版本说明
Flutter(ohos 版)3.44.9+ohos-0.0.1-canary1主验证环境,真机实测
编译 SDK26.0.0(26)DevEco Studio 26.0.0 自带
最低兼容 SDK5.0.0(12)工程最低 compatibleSdkVersion
真机OpenHarmony 6.1.1.120API 24,arm64,设备 ID 4UQ9K25508013016

在这里插入图片描述

在这里插入图片描述

编辑用户

两点提醒:

  1. 编译鸿蒙目标需要使用 ohos 版 Flutter SDK,普通 Flutter SDK 无法编译 ohos 产物;
  2. 鸿蒙剪贴板服务从 API 9(OpenHarmony 3.x)起提供 clearData() 接口,本插件最低兼容 5.0.0(12),覆盖面足够广。

HarmonyOS 技术点:compatibleSdkVersion 与 API Level

鸿蒙工程通过 build-profile.json5 中的 compatibleSdkVersion 字段声明应用最低运行的系统版本。该字段决定了编译产物的 API 门槛:设为 5.0.0(12) 意味着应用可以在 API 12 及以上的设备上安装运行。注意带括号的旧格式(如 5.0.0(12))与不带括号的新格式(如 26.0.0)在编译器中的处理方式不同,混用时需留意。本文实测环境的宿主工程配置为 5.0.0(12),真机 API 24,完全满足运行要求。


四、引入依赖

进入工程目录,在 pubspec.yaml 中添加 git 依赖:

dependencies:
  custom_clipboard:
    git:
      url: https://atomgit.com/oh-flutter/custom_clipboard.git
      # ref: 根据下方表格选择不同框架适配的 TAG 版本
      ref: 0.0.6-ohos-1.0.0-beta.1

执行命令拉取依赖:

flutter pub get

TAG 命名规则:原库版本-ohos-版本号-beta.x

Flutter 框架版本TAG 名称分支名
3.440.0.6-ohos-1.0.0-beta.1main

说明:该 TAG 已在 3.44.9+ohos-0.0.1-canary1 真机上实测通过,宿主工程 compatibleSdkVersion 设为 5.0.0(12) 即可在 API 24 真机运行。原库的 Dart 层 API 与上游完全一致,适配过程对 Dart 代码零改动。

联邦插件结构说明:custom_clipboard 采用 Flutter 官方推荐的 federated plugin 结构,拆分为四个包:custom_clipboard(面向用户的主包,含 Dart API)、custom_clipboard_platform_interface(平台接口抽象)、custom_clipboard_android(Android 实现)、custom_clipboard_ios(iOS 实现)。鸿蒙适配版没有新建独立的 custom_clipboard_ohos 联邦包,而是在主包内内联了 ohos/ 目录——这是因为插件仅有一个方法通道和一个方法,联邦化拆包反而增加维护成本。


五、代码接入

5.1 导入库

import 'package:custom_clipboard/custom_clipboard.dart';
import 'package:flutter/services.dart';

导入后即可使用 getClipboard()clearClipboard() 两个顶层函数。如需直接调用原生插件接口,还需导入平台接口包:

import 'package:custom_clipboard_platform_interface/custom_clipboard_platform_interface.dart';

5.2 读取剪贴板

final String? data = await getClipboard();
debugPrint('剪贴板内容: $data');

getClipboard() 内部直接调用 Flutter 引擎内置的 Clipboard.getData(Clipboard.kTextPlain),在所有平台上行为一致。在鸿蒙设备上,由于 ohos.permission.READ_PASTEBOARD 是系统受限权限(restricted),第三方应用在调试签名下无法获得该权限,引擎在权限校验失败时对 Dart 返回 null。因此,当前适配版本在受限权限设备上 getClipboard() 恒为 null

HarmonyOS 技术点:受限权限(Restricted Permission)

鸿蒙系统的权限分为三个级别:normal(普通权限,声明即获得)、system_basic(系统基本权限)、system_core(系统核心权限)。此外还有一类特殊权限——restricted(受限权限),ohos.permission.READ_PASTEBOARD 就属于此类。受限权限不能通过简单的 requestPermissions 获得,必须使用携带 ACL(Access Control List)的正式签名证书方可声明。这意味着调试签名应用即使声明了该权限,安装时也会报错 9568289 grant request permissions failed。这一机制是鸿蒙系统对用户隐私的保护——防止恶意应用静默读取剪贴板中的敏感信息。

5.3 清空剪贴板

方式一:库级别 API(引擎路径)

await clearClipboard();

在鸿蒙设备上,clearClipboard() 的 Dart 实现走 else 分支(因为 Platform.isAndroid == falsePlatform.isIOS == false),调用 Clipboard.setData(const ClipboardData(text: '')),最终由引擎写入一条空文本记录到系统剪贴板。这能清空剪贴板中的文本内容,但并非真正意义上的"清除记录"。

以下是库级别的 Dart 实现代码,逐段解析:

CustomClipboardPlatform get _method => CustomClipboardPlatform.instance;

/// 读取剪贴板数据
Future<String?> getClipboard() async {
  final data = await Clipboard.getData(Clipboard.kTextPlain);
  return data?.text;
}

/// 清空剪贴板
Future<void> clearClipboard() async {
  if (Platform.isAndroid || Platform.isIOS) {
    // Android / iOS 走平台原生接口(ClipboardManager.clearPrimaryClip / UIPasteboard.string = "")
    await _method.clearClipboard();
  } else {
    // 其余平台(含鸿蒙)走引擎内置 Clipboard.setData('') 路径
    await Clipboard.setData(const ClipboardData(text: ''));
  }
}

上述代码的逻辑非常清晰:getClipboard() 始终委托给 Flutter 引擎的 Clipboard.getData,不做平台分支;clearClipboard() 则根据当前平台决定走原生通道还是引擎路径。在 Android 和 iOS 上,由于 Platform.isAndroid / Platform.isIOStrue,会走到 _method.clearClipboard()CustomClipboardPlatform.instance.clearClipboard(),命中各平台的原生实现。而在鸿蒙上,由于 ohos fork 的 dart:io 新增了 Platform.isOhosPlatform.isAndroidPlatform.isIOS 均为 false,因此走 else 分支的引擎路径。

方式二:原生插件直调(真正清空)

await CustomClipboardPlatform.instance.clearClipboard();

这行代码会通过 MethodChannel 'custom_clipboard' 直接命中鸿蒙侧 ArkTS 插件的 pasteboard.getSystemPasteboard().clearData(),实现与 Android clearPrimaryClip() 语义一致的真正清空。如果你需要确保剪贴板被彻底清除而非写入空文本,请使用这种方式。

两种清空方式的对比

调用方式鸿蒙端实际行为是否真正清空残留空记录触发粘贴通知
clearClipboard()(库 API)引擎 Clipboard.setData('')否(写入空文本)可能
CustomClipboardPlatform.instance.clearClipboard()原生 pasteboard.clearData()

在安全敏感场景(密码管理、验证码复制后清除),建议使用原生插件直调方式。

5.4 跨平台行为

同一对接口在各端的行为:

平台写入剪贴板清空剪贴板读取剪贴板
Android引擎 Clipboard.setDataClipboardManager.clearPrimaryClip()(API 28+)引擎 Clipboard.getData(无限制)
iOS引擎 Clipboard.setDataUIPasteboard.general.string = ""引擎 Clipboard.getData(无限制)
OpenHarmony / HarmonyOS引擎 Clipboard.setData原生 pasteboard.clearData()(真正清空)引擎 Clipboard.getData(受限权限,返回 null)
桌面端与 Web引擎 Clipboard.setData引擎 Clipboard.setData('')引擎 Clipboard.getData

5.5 实战:复制验证码后自动清空剪贴板

实际业务中常见的场景是:用户复制了验证码或临时密码,应用在一段时间后自动清空剪贴板以保护隐私。下面是一个可直接使用的组件:

import 'package:flutter/material.dart';
import 'package:custom_clipboard/custom_clipboard.dart';
import 'package:custom_clipboard_platform_interface/custom_clipboard_platform_interface.dart';

class SecureClipboardHelper {
  /// 将文本写入剪贴板,并在 [clearAfter] 秒后真正清空。
  static Future<void> copyAndAutoClear(
    String text, {
    Duration clearAfter = const Duration(seconds: 30),
    VoidCallback? onCleared,
  }) async {
    // 写入系统剪贴板
    await Clipboard.setData(ClipboardData(text: text));
    debugPrint('已复制到剪贴板,$clearAfter 后自动清空');

    // 延迟后调用原生插件真正清空
    Future.delayed(clearAfter, () async {
      // 直接调用原生插件 pasteboard.clearData(),真正清除剪贴板记录
      await CustomClipboardPlatform.instance.clearClipboard();

      // 验证清空结果(hasStrings 走引擎 hasData,无需受限权限)
      final bool has = await Clipboard.hasStrings();
      debugPrint('剪贴板已清空,状态: ${has ? "仍有内容" : "已无内容"}');
      onCleared?.call();
    });
  }
}

上述组件的核心设计思路是:先用引擎 Clipboard.setData 写入文本(这一步在所有平台都可用且无需任何权限),然后在延迟后调用 CustomClipboardPlatform.instance.clearClipboard() 触发原生 pasteboard.clearData() 实现真正清空。最后通过 Clipboard.hasStrings() 验证清空结果——这个查询走引擎的 hasData 路径,不受 READ_PASTEBOARD 受限权限影响。

HarmonyOS 技术点:Clipboard.hasStrings() 为何不受限权影响?

鸿蒙 pasteboard 服务的 hasData() 方法只需要检查剪贴板中是否存在数据记录,不读取具体内容,因此不需要 READ_PASTEBOARD 权限。Flutter 引擎的 Clipboard.hasStrings 在鸿蒙端最终调用 pasteboard.hasData(),返回 Future<bool>。而 getData() 方法需要返回剪贴板中的实际文本内容,因此受受限权限保护。这是一个微妙但重要的区别:你可以知道"剪贴板里有没有东西",但不能在未授权时知道"里面具体是什么"。


六、运行与验证

以下为 demo 工程的真机实测记录。仓库中 example 的 Bundle Name 为 com.example.ohos_example_scaffold,签名配置使用 DevEco Studio 自动签名。

设备项
机型OpenHarmony 真机
设备 ID4UQ9K25508013016
系统版本OpenHarmony 6.1.1.120
API 版本24
架构arm64

HarmonyOS 技术点:HAR 模块与宿主工程

custom_clipboard 的鸿蒙侧原生代码以 HAR(Harmony Archive)模块形式打包,类似于 Android 的 AAR。HAR 模块在 module.json5 中声明 "type": "har",可以被宿主工程通过 oh-package.json5 依赖引入。Flutter 的 ohos 适配层会自动在宿主工程的 GeneratedPluginRegistrant 中注册插件类,无需手动编写注册代码——这一机制与 Android 的 GeneratedPluginRegistrant.java 如出一辙。

在这里插入图片描述

6.1 验证一:Copy(写入剪贴板)

构建 hap 后安装到真机并启动:

# 构建 hap(debug,含签名)
flutter build hap --debug

# 安装到真机
hdc install entry-default-signed.hap

# 启动 demo
hdc shell aa start -b com.example.ohos_example_scaffold -a EntryAbility

应用启动后,在输入框中输入文本(如 HelloOHOS2026),点击 Copy Input to Clipboard 按钮。通过 hilog 观察系统剪贴板服务日志:

hdc shell "timeout 5 hilog | grep -E 'pasteboard_service|Clipboard'"

实测日志输出:

PlatformMethodCallback --> Received 'Clipboard.setData' message.
pasteboard_service/PBS: SetPasteData# fd=20, agedTime_=3600000, rawDataSize=377
pasteboard_service/PBS: WritePasteData# set local data, dataSize=377
pasteboard_service/PBS: CallbackExit# pid:39992, uid:20020216, cmd:101, ret:0

在这里插入图片描述

系统剪贴板服务(pasteboard_service)确认写入成功(ret:0)。同时,系统输入法面板的"来自剪贴板"气泡直接显示了刚复制的文本,从 UI 层面佐证剪贴板内容已真实落盘。Example 界面日志区同步显示 复制内容: "HelloOHOS2026"

日志解读SetPasteData 是 pasteboard 服务的写入入口,rawDataSize=377 表示写入数据的原始字节大小(含序列化开销),agedTime_=3600000 表示该条数据的过期时间为 3600 秒(1 小时)。CallbackExit# ret:0 表示写入操作的返回码为 0(成功)。进程 ID 39992 和 UID 20020216 标识了调用方应用。

6.2 验证二:Clear(清空剪贴板)

重新拉起 demo,点击 Clear Clipboard 按钮。该按钮内部先调用库级别 clearClipboard()(引擎路径,写入空文本),再调用 CustomClipboardPlatform.instance.clearClipboard()(原生路径,真正清空)。

实测日志输出:

PlatformMethodCallback --> Received 'Clipboard.setData' message.
# ... 引擎路径写入空文本 ...
PlatformMethodCallback --> Received 'Clipboard.hasStrings' message.
# 原生插件 pasteboard.clearData() 执行
CustomClipboardPlugin: clearClipboard success
# 清空后查询状态
PlatformMethodCallback --> Received 'Clipboard.hasStrings' message.

清空后,界面日志区显示 剪贴板状态: 无内容Clipboard.hasStrings() 返回 false。原生插件的 hilog 输出 clearClipboard success,确认 pasteboard.getSystemPasteboard().clearData() 执行成功。

在这里插入图片描述

6.3 验证三:Get(读取剪贴板)

点击 Get Clipboard 按钮,尝试读取剪贴板内容:

PlatformMethodCallback --> Received 'Clipboard.getData' message.
pasteboard_service/PBS: IsPermissionGranted# permission denied, perm=ohos.permission.READ_PASTEBOARD

系统剪贴板服务返回权限拒绝(permission denied),引擎在权限校验失败时对 Dart 返回 null。Example 界面日志区显示 读取剪贴板: "null",同时常驻提示条显示受限权限说明文本。这是系统安全机制的正确表现,不是插件缺陷(机制解释见 FAQ Q1)。

实测结论:

验证点结果
应用启动,Flutter 页面正常渲染通过
点击 Copy,文本成功写入系统剪贴板通过
系统输入法"来自剪贴板"气泡展示已复制文本通过
点击 Clear,原生 pasteboard.clearData() 执行成功通过
清空后 Clipboard.hasStrings() 返回 false通过
点击 Get,getClipboard() 返回 null(受限权限)符合预期(见 FAQ Q1)
全程无需申请任何权限即可完成写入与清空通过

在这里插入图片描述


七、工作原理

整个调用链路如下:

Dart: clearClipboard() / CustomClipboardPlatform.instance.clearClipboard()
  ├─ 引擎路径: Clipboard.setData('')  →  pasteboard.setData(空文本)
  └─ 原生路径: MethodChannel('custom_clipboard')
                → ArkTS: CustomClipboardPlugin.onMethodCall
                  → pasteboard.getSystemPasteboard().clearData()
                    → 系统剪贴板服务彻底清除所有记录
Dart: getClipboard()
  → 引擎: Clipboard.getData(Clipboard.kTextPlain)
    → pasteboard.getData()
      → 权限校验: ohos.permission.READ_PASTEBOARD
        ├─ 有权限 → 返回剪贴板文本内容
        └─ 无权限 → 引擎返回 null(不抛异常)

HarmonyOS 技术点:MethodChannel 与 FlutterPlugin

Flutter 的 MethodChannel 是 Dart 与原生平台之间的双向通信桥梁。在鸿蒙端,Flutter 引擎提供了 FlutterPluginMethodCallHandler 两个接口:FlutterPlugin 负责插件的生命周期管理(onAttachedToEngine / onDetachedFromEngine),MethodCallHandler 负责处理来自 Dart 的方法调用(onMethodCall)。custom_clipboard 的 ArkTS 插件同时实现了这两个接口——在 onAttachedToEngine 中创建 MethodChannel 并设置处理器,在 onMethodCall 中根据方法名分发到具体的实现方法。

7.1 ArkTS 插件实现逐段解析

鸿蒙侧插件的核心实现包含以下几个关键部分,逐段解析如下。

第一段:导入与类声明

import {
  FlutterPlugin,
  FlutterPluginBinding,
  MethodCall,
  MethodCallHandler,
  MethodChannel,
  MethodResult,
} from '@ohos/flutter_ohos';
import pasteboard from '@ohos.pasteboard';
import hilog from '@ohos.hilog';

const TAG = 'CustomClipboardPlugin';

export default class CustomClipboardPlugin implements FlutterPlugin, MethodCallHandler {
  private channel: MethodChannel | null = null;
  // ...
}

这段代码完成了三件事:从 @ohos/flutter_ohos 导入 Flutter 鸿蒙适配层提供的插件接口类型;从 @ohos.pasteboard 导入系统剪贴板模块;从 @ohos.hilog 导入日志模块。CustomClipboardPlugin 类声明实现 FlutterPluginMethodCallHandler 两个接口——前者管理插件生命周期,后者处理方法调用。channel 字段持有 MethodChannel 实例,初始化为 null,在引擎挂载时赋值。

HarmonyOS 技术点:@ohos/flutter_ohos 适配层

这是 Flutter 鸿蒙 fork 提供的核心适配包,封装了 Dart ↔ ArkTS 通信所需的所有基础设施。FlutterPluginBinding 提供了 getBinaryMessenger() 方法,用于获取消息信使实例,MethodChannel 的创建依赖它。MethodCall 封装了来自 Dart 的方法调用信息(方法名 + 参数),MethodResult 是回传结果的回调对象——注意它只允许回复一次,重复调用会抛异常。

第二段:生命周期管理

getUniqueClassName(): string {
  return "CustomClipboardPlugin"
}

onAttachedToEngine(binding: FlutterPluginBinding): void {
  hilog.info(0x0000, TAG, '=== onAttachedToEngine ===');
  this.channel = new MethodChannel(binding.getBinaryMessenger(), "custom_clipboard");
  this.channel.setMethodCallHandler(this)
}

onDetachedFromEngine(binding: FlutterPluginBinding): void {
  hilog.info(0x0000, TAG, '=== onDetachedFromEngine ===');
  if (this.channel != null) {
    this.channel.setMethodCallHandler(null)
    this.channel = null
  }
}

getUniqueClassName() 返回插件类的唯一标识,需与 pubspec.yaml 中的 pluginClass 字段保持一致,Flutter 鸿蒙适配层通过它来匹配和注册插件。onAttachedToEngine 在引擎加载插件时调用:通过 binding.getBinaryMessenger() 获取消息信使,创建名为 "custom_clipboard" 的 MethodChannel,并将当前实例设为方法调用处理器。onDetachedFromEngine 在引擎卸载插件时调用:清理 handler 引用并将 channel 置空,防止内存泄漏。

第三段:方法分发与清空实现

onMethodCall(call: MethodCall, result: MethodResult): void {
  if (call.method == "clearClipboard") {
    this.clearClipboard(result);
  } else {
    result.notImplemented()
  }
}

private async clearClipboard(result: MethodResult): Promise<void> {
  try {
    await pasteboard.getSystemPasteboard().clearData();
    hilog.info(0x0000, TAG, 'clearClipboard success');
    result.success(null)
  } catch (e) {
    hilog.error(0x0000, TAG, 'clearData failed: %{public}s', JSON.stringify(e));
    result.error('-1', 'clearData failed: ' + JSON.stringify(e), null)
  }
}

onMethodCall 是方法调用的入口:检查方法名是否为 "clearClipboard",匹配则委托给私有方法 clearClipboard(result),不匹配则调用 result.notImplemented() 回复"方法未实现"。

clearClipboard 方法标记为 async,返回 Promise<void>。核心逻辑只有一行:pasteboard.getSystemPasteboard().clearData()getSystemPasteboard() 返回系统剪贴板单例实例,clearData() 是异步方法,返回 Promise<void>,执行成功后剪贴板中的所有记录被彻底清除。成功时调用 result.success(null) 回复 Dart 侧(与 Android/iOS 的返回值语义一致),失败时通过 result.error() 回传错误信息。

为什么 result.success(null) 而不是 result.success(true)

库的 Dart 层 clearClipboard() 返回 Future<void>,不关心具体返回值。Android 侧返回 success(null),iOS 侧返回 success(nil),鸿蒙侧同样返回 success(null) 保持跨端一致。MethodResult.success() 的参数类型是 Object?,传 null 表示"操作完成,无返回数据"。

7.2 插件注册机制

鸿蒙侧的插件注册是自动完成的。Flutter 鸿蒙适配层在构建时会扫描 pubspec.yaml 中的 ohos: pluginClass 配置,自动生成 GeneratedPluginRegistrant 文件:

import { FlutterEngine, Log } from '@ohos/flutter_ohos';
import CustomClipboardPlugin from 'custom_clipboard';

export class GeneratedPluginRegistrant {
  static registerWith(flutterEngine: FlutterEngine) {
    try {
      flutterEngine.getPlugins()?.add(new CustomClipboardPlugin());
    } catch (e) {
      Log.e(TAG, "Tried to register plugins with FlutterEngine failed.");
      Log.e(TAG, "Received exception while registering", e);
    }
  }
}

宿主工程的 EntryAbilityconfigureFlutterEngine 中调用 GeneratedPluginRegistrant.registerWith(flutterEngine),即可将所有插件注册到引擎。整个注册链路为:

EntryAbility.configureFlutterEngine()
  → GeneratedPluginRegistrant.registerWith(flutterEngine)
    → flutterEngine.getPlugins().add(new CustomClipboardPlugin())
      → CustomClipboardPlugin.onAttachedToEngine(binding)
        → new MethodChannel(messenger, "custom_clipboard")
          → setMethodCallHandler(this)

HarmonyOS 技术点:FlutterAbility 与 EntryAbility

鸿蒙 Flutter 应用的入口 Ability 需要继承 FlutterAbility(由 @ohos/flutter_ohos 提供),而非标准的 UIAbilityFlutterAbility 内部封装了 FlutterEngine 的初始化、Surface 注册、路由管理等逻辑。子类只需重写 configureFlutterEngine 方法,在其中调用 GeneratedPluginRegistrant.registerWith 即可完成所有原生插件的注册。这一设计与 Android 端的 FlutterActivity.configureFlutterEngine 高度对称。

7.3 Dart 层路由逻辑

库的 Dart 代码一行未改,但其在鸿蒙上的实际路由与 Android/iOS 不同。关键在平台判定:

// 库的主文件(原样保留,零改动)
Future<void> clearClipboard() async {
  if (Platform.isAndroid || Platform.isIOS) {
    // Android / iOS → 原生平台接口
    await _method.clearClipboard();
  } else {
    // 鸿蒙 / 桌面 / Web → 引擎 Clipboard.setData('')
    await Clipboard.setData(const ClipboardData(text: ''));
  }
}

ohos fork 的 dart:io 新增了 Platform.isOhos(底层实现为 operatingSystem == 'ohos'),因此鸿蒙设备上 Platform.isAndroid == falsePlatform.isIOS == falseclearClipboard()else 分支的引擎路径。

但这并不意味着原生插件通道 custom_clipboard 没有用——直接调用 CustomClipboardPlatform.instance.clearClipboard() 时,平台接口的默认实现 MethodChannelCustomClipboard 会通过 MethodChannel "custom_clipboard" 向原生侧发送方法调用,命中 ArkTS 插件的 onMethodCall,最终执行 pasteboard.clearData()

MethodChannelCustomClipboard.clearClipboard()
  → MethodChannel('custom_clipboard').invokeMethod('clearClipboard')
    → ArkTS: CustomClipboardPlugin.onMethodCall
      → pasteboard.getSystemPasteboard().clearData()

平台接口的默认实现CustomClipboardPlatform 继承自 PlatformInterface,其默认实例为 MethodChannelCustomClipboard,后者使用的通道名恰好是 "custom_clipboard"——与鸿蒙原生插件注册的通道名完全一致。这意味着即使不注册任何平台特定实现(不调用 registerWith),直接调用 CustomClipboardPlatform.instance.clearClipboard() 也能命中鸿蒙原生插件。这是一种"隐式匹配"设计,降低了使用门槛。

以下是操作的视屏,可以参考一下:

清空后状态(需要授权)

八、常见问题

Q1:getClipboard() 在鸿蒙上返回 null 是怎么回事?

鸿蒙系统的 ohos.permission.READ_PASTEBOARD 属于系统受限权限(restricted),第三方应用必须使用携带 ACL 的正式签名证书才能声明该权限。调试签名应用声明后会导致安装失败(9568289 grant request permissions failed),未声明时引擎在权限校验失败时对 Dart 返回 null。因此,当前适配版本在受限权限设备上 getClipboard() 恒为 null。写入与清空不受影响。发布版使用含 ACL 的正式签名证书声明该权限后,getClipboard() 即可正常读取。

Q2:clearClipboard()CustomClipboardPlatform.instance.clearClipboard() 有什么区别?

clearClipboard() 是库级别的封装,在鸿蒙上走引擎 Clipboard.setData('') 路径(写入空文本),能清空内容但剪贴板中仍保留一条空记录。CustomClipboardPlatform.instance.clearClipboard() 直接命中 ArkTS 原生插件的 pasteboard.clearData(),彻底清除剪贴板中的所有记录,不留任何痕迹。在安全敏感场景(如复制密码后清除),建议使用原生插件直调方式。

Q3:声明了 READ_PASTEBOARD 权限后安装失败怎么办?

module.json5requestPermissions 中声明 ohos.permission.READ_PASTEBOARD 后,需要同时配置 reason(引用字符串资源说明用途)和 usedScene(声明使用场景和时机)。即使格式正确,调试签名应用仍会因 ACL 缺失而安装失败(9568289)。解决方案是:调试阶段移除该权限声明,在 example 界面通过提示条告知用户读取限制;发布时使用含 ACL 的正式签名证书声明该权限。

在这里插入图片描述

在这里插入图片描述

Q4:Clipboard.hasStrings() 为什么不受限权影响?

Clipboard.hasStrings() 在鸿蒙端最终调用 pasteboard.hasData(),该方法只检查剪贴板中是否存在数据记录,不读取具体内容,因此不需要 READ_PASTEBOARD 权限。而 getData() 方法需要返回剪贴板中的实际文本内容,受受限权限保护。这一设计允许应用在不拥有读取权限的情况下仍能感知剪贴板状态变化,用于决定是否显示"粘贴"按钮等 UI 元素。

Q5:为什么选择内联 ohos/ 目录而不是新建联邦包?

custom_clipboard 的联邦结构包含 custom_clipboardcustom_clipboard_androidcustom_clipboard_ioscustom_clipboard_platform_interface 四个包。鸿蒙适配没有新建独立的 custom_clipboard_ohos 联邦包,而是在主包内内联了 ohos/ 目录。原因是插件仅有一个方法通道和一个方法,联邦化拆包反而增加维护成本。同时,内联方式与社区其他鸿蒙化库的结构保持一致。pubspec.yaml 中通过 ohos: pluginClass: CustomClipboardPlugin 声明插件类名,Flutter 鸿蒙适配层会自动发现并注册。

Q6:pasteboard.clearData() 清空的是全部记录还是仅当前记录?

pasteboard.getSystemPasteboard().clearData() 清除的是系统剪贴板中的全部数据记录。鸿蒙系统剪贴板在同一时间只持有一条有效记录(最近一次写入的内容),clearData() 会将这条记录彻底移除,而非用空文本覆盖。清空后 hasData() 返回 false,其他应用在尝试读取时也会发现剪贴板为空。


九、结语

回顾一下:在 pubspec.yaml 中以 git TAG 引入 custom_clipboard,调用 clearClipboard()CustomClipboardPlatform.instance.clearClipboard() 即可在鸿蒙 App 内操作系统剪贴板。前者走引擎路径写入空文本,后者走原生插件 pasteboard.clearData() 实现真正清空——两种方式在鸿蒙设备上均无需申请任何权限。getClipboard() 虽受系统受限权限限制在调试签名下返回 null,但 Clipboard.hasStrings() 可正常查询剪贴板状态。同一段代码在 Android、iOS 上也各自生效,Dart 层零改动。

总结对比

维度引擎路径 clearClipboard()原生路径 CustomClipboardPlatform.instance.clearClipboard()
鸿蒙端行为pasteboard.setData('') 写入空文本pasteboard.clearData() 彻底清除记录
是否真正清空否(残留空记录)
权限要求
跨平台一致性是(所有平台走引擎)是(各端走原生 API)
推荐场景一般场景安全敏感场景
维度getClipboard()Clipboard.hasStrings()
鸿蒙端行为pasteboard.getData() 读取内容pasteboard.hasData() 检查是否存在
权限要求READ_PASTEBOARD(受限权限)
调试签名下返回值nullFuture<bool>(正常工作)
发布版(含 ACL 签名)正常返回文本正常返回 Future<bool>

核心要点:写入与清空无需任何权限,读取受系统受限权限保护。这是鸿蒙系统对用户剪贴板隐私的安全设计,而非插件缺陷。

使用中发现任何问题,欢迎到配套仓库提 Issue,也欢迎发 PR 共建。

相关链接

欢迎加入 CPF-Flutter 鸿蒙社区,社区入口、环境搭建指南和本文相关链接统一放在这里:

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐