开发工具: 华为云码道

本文配套仓库: 上游 ShahzodAtabayev/ui_paste_component;鸿蒙适配改动位于本地仓库的 ohos/example/ohos/
鸿蒙适配后仓库https://atomgit.com/oh-flutter/ui_paste_component

ui_paste_component 将系统级“粘贴”控件封装为 Flutter 平台视图组件:在 iOS 上对应系统 UIPasteControl,用户点击后读取系统剪贴板并把文本回传给 Dart。本文以 ui_paste_component 0.0.6 为例,介绍源码准备、OHOS 工程配置、ArkTS 实现和 example 真机运行。

插件目前支持 ohos 平台,上游源码位于 GitHub 仓库。文中的代码以上游 master 分支提交 c544a51735ce7dbf39aad9da4602f5957c73c228 为适配基线。

在这里插入图片描述
OHOS 适配https://atomgit.com/oh-flutter/ui_paste_component改动当前在 main 分支工作区,发布 TAG 为 0.0.6-ohos-1.0.0-beta.1

在这里插入图片描述


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

Example 启动复制后粘贴当前系统剪切板授权系统剪贴板
Example 启动时界面复制后在目标输入框粘贴内容(有一个黄色层)系统剪贴板显示内容

以下是操作的视屏(因大小问题,一个视屏分为三断),可以参考一下:

清空后状态(需要授权) 清空后状态(需要授权) 清空后状态(需要授权)


一、插件简介与适配目标

系统粘贴控件是输入体验的一部分。iOS 16 起,系统提供 UIPasteControl:应用把这类控件放进输入框的长按菜单或表单中,用户点击后系统读取剪贴板并回调文本。ui_paste_component 把这一能力封装成 Flutter 组件 UIPastComponent,业务通过 onPasted 回调拿到粘贴文本,不需要自己处理剪贴板读取。

例如,登录页可以把粘贴控件放进验证码输入框的长按菜单,用户复制验证码后一键粘贴;表单页也可以在任意输入区域旁提供粘贴入口,替代需要长按呼出的文本菜单。

粘贴文本通过平台视图加回调的方式获取:UIPastComponent 在页面中嵌入原生视图,用户点击原生按钮时,原生侧读取系统剪贴板,并通过 MethodChannel 把文本回传给 Dart。OpenHarmony 没有对应的系统粘贴按钮控件,适配时用 ArkUI 按钮复刻控件外观,点击时先申请剪贴板读取权限,再完成读取和回传。


二、环境准备

环境搭建参考社区文档: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 分支0.0.6-ohos-1.0.0-beta.1CPF-Flutter 对应开发分支
Dart SDK3.12.2Dart 语言与包管理环境
HarmonyOS 开发套件26.0.0(API 26)开发套件版本及对应的 API 级别
compatibleSdkVersion5.1.0(18)当前工程声明的最低兼容版本
插件版本0.0.6pubspec.yaml 中的包版本
原生语言ArkTSHarmonyOS 插件实现
插件产物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 没有显式声明 compileSdkVersiontargetSdkVersion,构建时按开发套件默认的 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.yamlLICENSE 和提交历史是否完整,并记录适配起点的提交号。如果上游本身托管在 AtomGit,也可以通过 Fork 获得自己的工作仓库。

ui_paste_component 的上游位于 GitHub。可以按上文流程把上游仓库导入 AtomGit 或其他 Git 托管平台,获得自己有写权限的工作仓库;本文直接从 GitHub 地址拉取代码。

3.2 将代码拉取到宿主机

在安装了 Flutter OH 和 DevEco Studio 的开发电脑上打开终端,进入准备存放项目的目录,执行:

git clone https://github.com/ShahzodAtabayev/ui_paste_component.git
cd ui_paste_component
pwd
ls
git remote -v
git status --short --branch
git rev-parse HEAD

在这里插入图片描述

git clone 会创建 ui_paste_component/ 目录;cd 后的位置就是下文所说的插件仓库根目录,这里应能看到 pubspec.yamllib/example/。本例的仓库名与 Dart 包名相同,都是 ui_paste_component

需要使用与本文相同的代码版本时,在没有未提交修改的仓库中切换到以下提交:

git switch --detach c544a51735ce7dbf39aad9da4602f5957c73c228

适配其他插件时,使用该插件对应版本的 tag 或 commit 作为分支起点。

在这里插入图片描述

图 1:在宿主机终端输入仓库拉取命令。

3.3 在仓库根目录创建适配分支

接着在 ui_paste_component/ 根目录创建分支。分支名统一使用 feat/ohos_库名称_版本号,库名称取 pubspec.yamlname,版本号取此次适配的基线版本。本例为:

git switch -c feat/ohos_ui_paste_component_0.0.6
git branch --show-current

如果该分支已存在,使用 git switch feat/ohos_ui_paste_component_0.0.6 切换即可。上游仓库的 master 分支是当前主线,v0.0.1 标签指向早期版本,不作为本次适配基线。

请添加图片描述

图 2:在 ui_paste_component 仓库根目录输入适配分支创建命令。

3.4 自动补全 OHOS 适配结构

分支创建后,仍在同一个插件根目录执行结构补全。以下命令适用于尚无 ohos/ 目录的既有 Flutter 平台插件

flutter create --template=plugin --platforms=ohos --project-name ui_paste_component .
git status --short
git diff -- pubspec.yaml lib example
  • --template=plugin 指定插件模板。
  • --platforms=ohos 指定需要补全的平台。
  • --project-name ui_paste_component 使用 Dart 包名,与 pubspec.yaml 中的 name 保持一致。
  • 最后的 . 表示在当前插件目录补全工程,不是另建一层 ui_paste_component/

该命令生成 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 ui_paste_component;已有插件使用上面的 . 在当前目录补全。

请添加图片描述

图 3:在插件根目录输入 OHOS 结构补全命令。

3.5 适配后的项目目录

适配后的关键目录如下:

ui_paste_component/
├── lib/
│   ├── ui_paste_component.dart
│   ├── ui_paste_component_method_channel.dart
│   └── ui_paste_component_platform_interface.dart
├── ohos/
│   ├── index.ets
│   ├── oh-package.json5
│   └── src/main/
│       ├── ets/components/plugin/
│       │   ├── UiPasteComponentPlugin.ets
│       │   ├── PasteComponentPlatformView.ets
│       │   └── PluginConstants.ets
│       ├── module.json5
│       └── resources/base/element/string.json
├── example/
│   ├── lib/main.dart
│   └── ohos/entry/
├── test/
└── pubspec.yaml

项目根目录如下,其中包含 ohos/example/,以及上游的 README.mdCHANGELOG.md

请添加图片描述

图 4:适配后的 ui_paste_component 项目根目录。

文件主要职责
lib/ui_paste_component.dart提供 UIPastComponent 组件入口并注册回调
lib/ui_paste_component_platform_interface.dart定义平台无关接口
lib/ui_paste_component_method_channel.dart实现 MethodChannel 通信与回调分发
UiPasteComponentPlugin.ets注册平台视图工厂和通道,实现运行时权限申请
PasteComponentPlatformView.ets渲染粘贴按钮并读取系统剪贴板
PluginConstants.ets集中维护通道名、视图类型和权限常量
插件 module.json5声明 HAR 模块
示例 entry module.json5声明宿主应用 Ability、设备类型和权限场景
example/lib/main.dart展示复制、长按菜单集成和剪贴板预览

四、Dart 接口与通道分析

OHOS 实现需要遵循 Dart 层已有的视图类型、方法名和回调约定。先阅读 lib/ui_paste_component.dartlib/ui_paste_component_platform_interface.dartlib/ui_paste_component_method_channel.dart,再在 ohos/src/main/ets/components/plugin/ 中实现对应的原生视图与通道。适配其他已有平台的插件时,也应保留其公开接口和其他平台实现。

本例的对应关系如下:

Dart 入口或模型通道协议OHOS 实现应保持的行为
UIPastComponent(onPasted)平台视图 paste_component + pasted 方法注册 PasteComponentPlatformView 并渲染粘贴按钮视图正常渲染,粘贴文本进入 onPasted
UiPasteComponentPlatform.setPasteMethodui_paste_component / pastedchannel.invokeMethod('pasted', text)原生回传文本,Dart 分发给当前回调
getPlatformVersionui_paste_component / getPlatformVersion返回 'OpenHarmony'保持模板方法可用

原生端需要保持这些名称一致。视图类型或通道名任何一端拼写不一致,都会出现“视图空白”或“收不到回调”等问题。

4.1 跨端架构与调用时序

Flutter 侧和 HarmonyOS 侧之间使用一条 MethodChannel 和一个平台视图:

  1. MethodChannel('ui_paste_component'):Dart 调用 getPlatformVersion,原生侧回推 pasted 粘贴文本;
  2. 平台视图 paste_component:Dart 通过 UiKitView 嵌入,原生侧用 ArkUI 按钮渲染。

Flutter 页面

UIPastComponent

UiKitView paste_component

ArkTS PlatformViewFactory

PasteComponentPlatformView

READ_PASTEBOARD 授权

pasteboard 剪贴板

MethodChannel ui_paste_component

MethodChannelUiPasteComponent

onPasted 回调

MethodChannel 承担一次性命令和文本回传,平台视图负责原生 UI 渲染和用户点击。

4.1.1 一次完整粘贴的时序
OpenHarmony pasteboard PasteComponentPlatformView UiPasteComponentPlugin.ets MethodChannelUiPasteComponent Flutter App OpenHarmony pasteboard PasteComponentPlatformView UiPasteComponentPlugin.ets MethodChannelUiPasteComponent Flutter App setPasteMethod(onPasted) UiKitView(paste_component) 注册视图并接线回调 渲染 Paste 按钮 点击按钮 requestPastePermission() 授权结果 getDataSync() 纯文本记录 onPasted(text) invokeMethod(pasted, text) onPasted(text) dispose() 清理回调

4.2 组件与回调模型:lib/ui_paste_component.dart

UIPastComponent 是平台视图的 Dart 载体,把原生控件嵌入页面并注册回调:

class UIPastComponent extends StatefulWidget {
  final Function(String pasted) onPasted;

  const UIPastComponent({
    super.key,
    required this.onPasted,
  });
}

class _UIPastComponentState extends State<UIPastComponent> {
  
  void initState() {
    super.initState();
    UiPasteComponentPlatform.instance.setPasteMethod(widget.onPasted);
  }

  
  Widget build(BuildContext context) {
    const String viewType = 'paste_component';
    final Map<String, dynamic> creationParams = <String, dynamic>{};
    return SizedBox(
      height: 68,
      width: double.infinity,
      child: UiKitView(
        viewType: viewType,
        creationParams: creationParams,
        layoutDirection: TextDirection.ltr,
        creationParamsCodec: const StandardMessageCodec(),
        hitTestBehavior: PlatformViewHitTestBehavior.translucent,
      ),
    );
  }
}
参数或成员含义
viewType'paste_component'与原生注册的视图类型一致
creationParamsMap当前无需向原生传参
creationParamsCodecStandardMessageCodec与原生工厂的编解码器一致
onPastedFunction(String pasted)每次粘贴成功触发一次

initState 中注册回调:组件挂载时把 onPasted 交给平台接口,后续原生回传的文本都会进入该回调。组件没有单独的取消注册接口,回调与视图生命周期在原生侧一并清理。

4.3 公开 API 与平台接口

UiPasteComponentPlatform 定义平台接口。业务通过 UIPastComponent 间接使用它,测试中也可以替换平台实现。接口定义如下:

abstract class UiPasteComponentPlatform extends PlatformInterface {
  UiPasteComponentPlatform() : super(token: _token);

  static final Object _token = Object();

  static UiPasteComponentPlatform _instance = MethodChannelUiPasteComponent();

  static UiPasteComponentPlatform get instance => _instance;

  static set instance(UiPasteComponentPlatform instance) {
    PlatformInterface.verifyToken(instance, _token);
    _instance = instance;
  }

  void setPasteMethod(Function(String arguments) pasteMethod) {
    throw UnimplementedError('setPasteMethod() has not been implemented.');
  }
}

setPasteMethod 的触发时机:

  • 组件 initState 时注册一次,替换之前的回调;
  • 原生每次回传 pasted 时调用,可以执行多次;
  • 页面销毁后原生不再回传,回调由原生视图 dispose 清理。

MethodChannelUiPasteComponent 是默认实现,详见 4.4。

4.4 Dart 通道协议分析

4.4.1 通道名称必须两端完全一致
final methodChannel = const MethodChannel('ui_paste_component');
const String viewType = 'paste_component';

原生侧把这两个名字收敛在 PluginConstants.ets 中:

export class PluginConstants {
  static readonly channelName: string = 'ui_paste_component';
  static readonly platformViewType: string = 'paste_component';
  static readonly pastedMethod: string = 'pasted';
  static readonly getPlatformVersionMethod: string = 'getPlatformVersion';
  static readonly readPasteboardPermission: Permissions = 'ohos.permission.READ_PASTEBOARD';
}

通道名称和视图类型属于跨语言协议。Dart 和 ArkTS 任何一端拼写不一致,都会出现“视图空白”或“收不到回调”等问题。

4.4.2 回调注册与文本接收
class MethodChannelUiPasteComponent extends UiPasteComponentPlatform {
  
  final methodChannel = const MethodChannel('ui_paste_component');

  Function(String arguments)? _paste;

  MethodChannelUiPasteComponent() {
    methodChannel.setMethodCallHandler((call) {
      switch (call.method) {
        case "pasted":
          _paste?.call(call.arguments.toString());
          break;
        default:
      }
      return Future.value();
    });
  }

  
  void setPasteMethod(Function(String arguments) pasteMethod) async {
    _paste = pasteMethod;
  }
}

_paste 保存最近一次注册的回调,原生回传的文本交给该回调处理。call.arguments.toString() 保证即使原生传来非字符串值,也能以文本进入业务层。setPasteMethod 只替换引用,不涉及原生状态,多次调用不会产生副作用。

4.4.3 视图销毁与回调清理

Dart 侧没有取消订阅命令。平台视图被移除时,原生 PasteComponentPlatformView.dispose() 清理回调引用:

dispose(): void {
  this.onPasted = null;
  this.requestPastePermission = null;
}

Flutter Engine 销毁时,onDetachedFromEngine 再清理 MethodChannel 的处理器,覆盖业务未主动释放的情况。

五、补全 OHOS 原生实现与工程配置

5.1 在 UiPasteComponentPlugin.ets 中实现原生能力

以点击粘贴为例:业务仍使用 UIPastComponent(onPasted: ...),视图类型仍为 paste_component。需要补全的是 UiPasteComponentPlugin.ets 中的平台视图注册,以及 PasteComponentPlatformView.ets 中的视图实现:渲染粘贴按钮,点击时申请剪贴板读取权限,读取系统剪贴板文本,再通过 pasted 方法回传 Dart。这样业务页面沿用原有组件即可在 OHOS 上完成粘贴。

UiPasteComponentPlugin 实现 FlutterPluginMethodCallHandlerAbilityAware。下面列出类中的主要成员和方法,完整文件还包含 getUniqueClassName() 等插件接口。

原生插件位于:

ohos/src/main/ets/components/plugin/UiPasteComponentPlugin.ets
ohos/src/main/ets/components/plugin/PasteComponentPlatformView.ets
ohos/src/main/ets/components/plugin/PluginConstants.ets
5.1.1 引入 Flutter 和 HarmonyOS 能力
import {
  FlutterPlugin,
  FlutterPluginBinding
} from '@ohos/flutter_ohos/src/main/ets/embedding/engine/plugins/FlutterPlugin';
import MethodChannel, {
  MethodCallHandler,
  MethodResult
} from '@ohos/flutter_ohos/src/main/ets/plugin/common/MethodChannel';
import MethodCall from '@ohos/flutter_ohos/src/main/ets/plugin/common/MethodCall';
import PlatformViewFactory from '@ohos/flutter_ohos/src/main/ets/plugin/platform/PlatformViewFactory';
import PlatformView from '@ohos/flutter_ohos/src/main/ets/plugin/platform/PlatformView';
import StandardMessageCodec from '@ohos/flutter_ohos/src/main/ets/plugin/common/StandardMessageCodec';
import common from '@ohos.app.ability.common';
import abilityAccessCtrl from '@ohos.abilityAccessCtrl';
import bundleManager from '@ohos.bundle.bundleManager';
import { UIAbility } from '@kit.AbilityKit';
import { AbilityAware, AbilityPluginBinding } from '@ohos/flutter_ohos/index';

其中:

  • FlutterPlugin 负责接入 Flutter Engine 生命周期;
  • MethodChannel 接收 Dart 命令并向 Dart 回推粘贴文本;
  • PlatformViewFactoryPlatformView 承载 paste_component 平台视图;
  • StandardMessageCodec 与 Dart 侧 creationParamsCodec 保持一致;
  • abilityAccessCtrl 在运行时申请 READ_PASTEBOARD 权限;
  • bundleManager 读取本应用 accessTokenId,用于权限检查;
  • AbilityAware 获取当前 UIAbility,权限弹窗需要它的上下文。
5.1.2 连接 Flutter Engine
class PasteComponentViewFactory extends PlatformViewFactory {
  private onViewCreated: (view: PasteComponentPlatformView) => void;

  constructor(onViewCreated: (view: PasteComponentPlatformView) => void) {
    super(StandardMessageCodec.INSTANCE);
    this.onViewCreated = onViewCreated;
  }

  create(context: common.Context, viewId: number, args: Any): PlatformView {
    const view: PasteComponentPlatformView = new PasteComponentPlatformView(viewId);
    this.onViewCreated(view);
    return view;
  }
}

onAttachedToEngine(binding: FlutterPluginBinding): void {
  this.channel = new MethodChannel(binding.getBinaryMessenger(), PluginConstants.channelName);
  this.channel.setMethodCallHandler(this);

  binding.getPlatformViewRegistry().registerViewFactory(PluginConstants.platformViewType,
    new PasteComponentViewFactory((view: PasteComponentPlatformView) => {
        view.onPasted = (text: string) => {
          this.channel?.invokeMethod(PluginConstants.pastedMethod, text);
        };
        view.requestPastePermission = () => this.requestReadPasteboardPermission();
      }));
}

registerViewFactorypaste_component 视图类型与工厂绑定:Dart 侧 UiKitView 创建视图时,工厂返回 PasteComponentPlatformView,并通过 onViewCreated 把“回传文本”和“申请权限”两个函数接到插件上。这一注册机制与 Android/iOS 的 PlatformView 注册对应。

5.1.3 渲染粘贴按钮视图
@Component
struct PasteComponentViewContent {
  view: PasteComponentPlatformView | null = null;

  build() {
    Button('Paste')
      .width('100%')
      .height('100%')
      .backgroundColor('#F6F6F6')
      .fontColor(Color.Black)
      .fontSize(17)
      .borderRadius(12)
      .onClick(() => {
        this.view?.pasteFromClipboard();
      })
  }
}

@Builder
function pasteComponentBuilder(params: Params) {
  PasteComponentViewContent({ view: params.platformView as PasteComponentPlatformView })
}

按钮样式对齐 iOS UIPasteControl 的默认外观:浅灰 #F6F6F6 背景、黑色文字、固定圆角。引擎通过 DynamicView 机制渲染这个 ArkUI 组件。

这里有一个真机验证过的坑:$r() 资源引用在 DynamicView 平台视图上下文中不能可靠解析,按钮会渲染成没有文字的空块。因此标签使用固定字符串 'Paste',不依赖资源管理器。

5.1.4 读取剪贴板并回传 Dart
async pasteFromClipboard(): Promise<void> {
  try {
    if (this.requestPastePermission !== null) {
      const granted: boolean = await this.requestPastePermission();
      if (!granted) {
        console.error(`${TAG} READ_PASTEBOARD permission not granted, paste skipped`);
        return;
      }
    }
    const systemPasteboard: pasteboard.SystemPasteboard = pasteboard.getSystemPasteboard();
    const pasteData: pasteboard.PasteData = systemPasteboard.getDataSync();
    if (pasteData.getPrimaryMimeType() === pasteboard.MIMETYPE_TEXT_PLAIN) {
      const text: string = pasteData.getPrimaryText();
      if (text.length > 0 && this.onPasted !== null) {
        this.onPasted(text);
      }
    }
  } catch (error) {
    const message: string = error instanceof Error ? error.message : String(error);
    console.error(`${TAG} pasteFromClipboard failed: ${message}`);
  }
}

读取前先通过插件申请 READ_PASTEBOARD:授权失败直接返回,不读取剪贴板。读取时只接受 MIMETYPE_TEXT_PLAIN 的首条记录,与 iOS 侧只读 UIPasteboard.general.string 的语义一致;空文本不回调,所有 pasteboard 调用都有异常保护。

5.1.5 处理命令与运行时权限申请
onMethodCall(call: MethodCall, result: MethodResult): void {
  try {
    switch (call.method) {
      case PluginConstants.getPlatformVersionMethod:
        result.success('OpenHarmony');
        break;
      default:
        result.notImplemented();
    }
  } catch (error) {
    const message: string = error instanceof Error ? error.message : String(error);
    result.error('0', `Unexpected plugin error: ${message}`, null);
  }
}

getPlatformVersion 是插件模板遗留的探活方法,Android/iOS 返回各自的系统版本,OHOS 侧返回 'OpenHarmony'。异常统一转成 result.error 传回 Dart。

READ_PASTEBOARDuser_grant 权限,需要在运行时申请:

private async requestReadPasteboardPermission(): Promise<boolean> {
  const ability: UIAbility | null = this.ability;
  if (ability === null) {
    return false;
  }
  const atManager = abilityAccessCtrl.createAtManager();
  const bundleInfo = bundleManager.getBundleInfoForSelfSync(
    bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION);
  const tokenId: number = bundleInfo.appInfo.accessTokenId;
  const grantStatus = atManager.checkAccessTokenSync(tokenId, PluginConstants.readPasteboardPermission);
  if (grantStatus === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) {
    return true;
  }
  const result = await atManager.requestPermissionsFromUser(
    ability.context, [PluginConstants.readPasteboardPermission]);
  return result.authResults.length > 0 &&
    result.authResults[0] === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED;
}

AbilityAwareonAttachedToAbility 保存当前 UIAbility,权限弹窗依赖它的 context。先 checkAccessTokenSync 判断是否已授权,已授权时不再重复弹窗;未授权时 requestPermissionsFromUser 弹出系统授权对话框。

5.1.6 Engine 解绑时释放资源
onDetachedFromEngine(binding: FlutterPluginBinding): void {
  try {
    if (this.channel !== null) {
      this.channel.setMethodCallHandler(null);
      this.channel = null;
    }
    this.ability = null;
  } catch (error) {
    const message: string = error instanceof Error ? error.message : String(error);
    console.error(`${TAG} onDetachedFromEngine failed: ${message}`);
  }
}

Flutter Engine 销毁时清理通道 Handler 和 UIAbility 引用。平台视图自身随视图树销毁,dispose 中清空 onPastedrequestPastePermission,覆盖业务未主动释放的情况。

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": "ui_paste_component",
    "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:reason_pasteboard",
        "usedScene": {
          "abilities": ["EntryAbility"],
          "when": "inuse"
        }
      }
    ]
  }
}

权限原因资源合并到 example/ohos/entry/src/main/resources/base/element/string.json 的现有 string 数组中:

{
  "string": [
    {
      "name": "reason_pasteboard",
      "value": "读取剪贴板内容,以便完成粘贴操作"
    }
  ]
}

权限声明和运行时授权是两个步骤。module.json5 中的声明不会自动完成运行时授权,点击粘贴按钮时由插件通过 requestPermissionsFromUser 弹出授权对话框。

5.3 注册并导出插件

pubspec.yaml 通过以下配置声明 OHOS 插件类:

flutter:
  plugin:
    platforms:
      ohos:
        pluginClass: UiPasteComponentPlugin

插件的 ohos/index.ets 需要导出实现:

import UiPasteComponentPlugin from './src/main/ets/components/plugin/UiPasteComponentPlugin';
export default UiPasteComponentPlugin;

执行 flutter pub get 和构建后,Flutter 工具会为应用生成插件注册代码,GeneratedPluginRegistrant.ets 中会出现 flutterEngine.getPlugins()?.add(new UiPasteComponentPlugin())。通常不应手工编辑该文件,因为下次构建可能覆盖它。

注册异常的排查步骤见第九节 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.mdOHOS 新增能力、适配版本、兼容限制与测试范围
LICENSE / NOTICE保留上游许可证;NOTICE 按许可证和原项目要求保留或补充
example/README.md依赖方式、运行目录、签名、操作步骤与效果图;覆盖复制与粘贴操作
pubspec.yamlohos/oh-package.json5核对包名、版本、插件注册、仓库地址、许可证和依赖
.gitignore忽略构建缓存及本机签名材料,不漏提交必要源码和配置

除了文档,docs/ 目录还包含真机运行截图与 hilog_real_device.log

docs/
├── OHOS_Adaptation_Blog.md
├── hilog_real_device.log
└── images/
    ├── ui_paste_demo_page.jpg
    ├── ui_paste_demo_page_before_allow.jpg
    └── ui_paste_permission_dialog.jpg

README.OpenSource 记录库本身的来源与版本。本例的包名为 ui_paste_component,版本为 0.0.6,采用 Apache-2.0 许可证;Flutter 和 HarmonyOS SDK 版本写入环境说明。

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 .metadata
git add example/lib/main.dart
git add README.md README.OpenSource README.OpenHarmony_CN.md
git add README.OpenHarmony.md CHANGELOG.OpenHarmony.md
git diff --cached --check
git diff --cached --stat
git diff --cached
git commit -m "feat: add OHOS support for ui_paste_component 0.0.6"
git remote -v
git branch --show-current
git push -u origin feat/ohos_ui_paste_component_0.0.6

DevEco 可能向 example/ohos/build-profile.json5 写入本机签名配置,需要在提交前从暂存内容中移除。推送时,origin 应指向自己有写权限的仓库,当前分支为 feat/ohos_ui_paste_component_0.0.6;上游仓库在 GitHub,无权限直接推送时先推送到自己的镜像。

推送后在托管平台发起合并请求,说明上游来源和版本、OHOS 实现范围、依赖及权限、测试环境、操作结果、已知限制,并附 Demo 运行说明或运行图。目标分支和评审流程以接收仓库要求为准。

七、使用根目录 example 演示接入

仓库自带 example/,可以直接用来调试插件和体验粘贴流程。

7.1 本地适配时使用路径依赖

当前 example/pubspec.yaml 的依赖是:

dependencies:
  flutter:
    sdk: flutter
  flutter_localizations:
    sdk: flutter
  ui_paste_component:
    path: ../

../ 相对于 example/pubspec.yaml 指向插件根目录,修改根目录插件后可直接联调。

7.2 通过 Git 依赖引入插件

业务应用通过 Git 引入时,将 ui_paste_componentpath 配置替换为下面的 Git 依赖。上游 GitHub 仓库尚未包含 OHOS 适配,先按第六节把自己的适配分支推送到有写权限的仓库,再固定到该分支:

dependencies:
  flutter:
    sdk: flutter
  ui_paste_component:
    git:
      url: https://atomgit.com/{your-org}/ui_paste_component.git
      ref: feat/ohos_ui_paste_component_0.0.6

使用自己的适配版本时,将 url 改为对应仓库。正式发布后可固定到 tag 或 commit。

从插件根目录执行:

cd example
flutter pub get
flutter pub deps

检查 example/pubspec.lockui_paste_component 的来源为 git,并核对 urlrefresolved-ref。同时检查没有 dependency_overridespubspec_overrides.yaml 将其覆盖回本地依赖,确认应用使用的是 Git 依赖。

7.3 调用组件实现粘贴入口

下面的页面把粘贴控件直接嵌入表单,可用于 example/lib/main.dart。先复制文本,再点击系统样式的粘贴按钮,文本会填入输入框:

import 'package:flutter/material.dart';
import 'package:ui_paste_component/ui_paste_component.dart';

void main() {
  runApp(const MaterialApp(home: PastePage()));
}

class PastePage extends StatefulWidget {
  const PastePage({super.key});

  
  State<PastePage> createState() => _PastePageState();
}

class _PastePageState extends State<PastePage> {
  final TextEditingController _controller = TextEditingController();
  String _pasted = '(尚未粘贴)';

  
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('粘贴组件')),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            TextField(
              controller: _controller,
              decoration: const InputDecoration(
                hintText: '粘贴的内容将显示在这里',
                border: OutlineInputBorder(),
              ),
            ),
            const SizedBox(height: 16),
            UIPastComponent(
              onPasted: (pasted) {
                if (!mounted) return;
                setState(() {
                  _controller.text = pasted;
                  _pasted = pasted;
                });
              },
            ),
            const SizedBox(height: 8),
            Text('最近一次粘贴:$_pasted'),
          ],
        ),
      ),
    );
  }
}

仓库中的完整 Demo 还提供复制预设文案、长按菜单集成和剪贴板预览:在 iOS 16+ 上,长按输入框弹出的菜单中“粘贴”项会被 UIPastComponent 替换;在 OHOS 上,组件已验证可以直接作为页面子节点渲染,按上面的方式嵌入即可。

7.4 页面退出时的资源处理

异步回调先检查 mounted,避免页面销毁后继续调用 setStateUIPastComponent 没有需要业务取消的订阅:回调引用由原生视图 dispose 清理,页面 dispose 中只需释放自己的 TextEditingController 后调用 super.dispose()

多个输入区域都需要粘贴入口时,注意当前版本的平台接口只保存最后一个注册的 onPasted 回调;可以由应用级服务持有一个分发函数,或保证同一时间只挂载一个 UIPastComponent

八、验证、构建与鸿蒙设备运行效果

8.1 分别验证插件与 example

从插件仓库根目录执行:

flutter pub get
flutter analyze
flutter test
cd example
flutter pub get
flutter analyze
flutter test

仓库 test/ 中的用例来自插件模板:断言默认平台实例为 MethodChannelUiPasteComponent,通道和版本号相关断言大部分被注释,未覆盖粘贴回传流程。测试通过不等于真机粘贴链路可用。

Dart 测试覆盖接口和页面逻辑,平台视图渲染、剪贴板读取及权限行为还需要在鸿蒙设备上验证。

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。真机安装应选择与当前设备匹配的已签名产物。example/ 目录还保留了三份 flutter_XX.log 崩溃报告,记录构建期间 hvigor 以退出码 255 失败的过程,可按第九节的检查项排查签名与 SDK 配置后重试。

8.5 在设备上测试粘贴操作

  1. 打开应用,确认环境信息卡显示 ohos 平台和 0.0.6 (ohos) 插件版本;
  2. 在复制输入框输入文本或点击预设文案,点击“复制”,确认提示“已写入系统剪贴板”;
  3. 点击页面中的 Paste 按钮,首次读取时确认系统弹出剪贴板读取授权对话框;
  4. 允许授权后,确认粘贴的文本填入目标输入框;
  5. 复制非文本内容或清空剪贴板后再点击,确认组件不回调、无崩溃;
  6. 拒绝授权后再次点击,确认可通过系统设置或再次弹窗恢复授权;
  7. 多次复制不同文本并粘贴,确认每次回调内容正确、无重复回调。

页面初始的“(尚未粘贴)”来自 Demo 默认值。点击粘贴后收到的文本才表示系统剪贴板读取结果。

8.6 鸿蒙设备运行效果

完成适配后,Flutter 应用可以在 OHOS 页面中渲染系统样式的粘贴按钮,点击按钮完成 权限申请剪贴板读取文本回传,并把粘贴内容填入输入框。

下图是本次适配在鸿蒙设备上运行后的实际效果。从左到右依次为:示例首页(复制区、目标输入框、剪贴板预览)、点击粘贴前的首页状态、桌面上的应用入口。


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

Example 启动复制后粘贴当前系统剪切板授权系统剪贴板
Example 启动时界面复制后在目标输入框粘贴内容(有一个黄色层)系统剪贴板显示内容

以下是操作的视屏(因大小问题,一个视屏分为三断),可以参考一下:

清空后状态(需要授权) 清空后状态(需要授权) 清空后状态(需要授权)

设备运行证据归档在 docs/hilog_real_device.logdocs/images/。如果本地测试时无法复现,先对比日志中是否出现以下关键字:UiPasteComponentPlugin 注册成功、READ_PASTEBOARD 权限申请、剪贴板读取结果和 pasted 方法调用。

粘贴能力依赖设备的剪贴板服务和权限框架。即使系统版本满足要求,不同型号也可能存在能力差异,需要在目标设备上测试。

九、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 打开:

ui_paste_component/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 能安装但点击粘贴无反应

按以下顺序检查:

  1. 页面中是否实际渲染出 Paste 按钮(平台视图是否注册成功);
  2. entry 是否声明 READ_PASTEBOARD 权限及 reason 资源;
  3. 点击后是否弹出授权对话框,或在系统设置中确认权限状态;
  4. hilog/控制台是否出现 “READ_PASTEBOARD permission not granted, paste skipped”;
  5. 系统剪贴板中是否有纯文本内容(组件只读取 MIMETYPE_TEXT_PLAIN);
  6. hilog/控制台是否出现 “pasteFromClipboard failed”;
  7. Dart 侧 onPasted 回调是否被 setPasteMethod 正确注册。

9.5 MissingPluginException

这通常表示 Dart 通道找不到已注册的原生插件。新增原生插件后需要重新构建应用。从仓库根目录执行:

cd example
flutter clean
flutter pub get
flutter run -d <device-id>

如果仍然出现,检查自动生成的插件注册文件中是否包含 UiPasteComponentPlugin,同时核对 pubspec.yamlohos/index.etsoh-package.json5

9.6 多个粘贴组件只有最后一个收到回调

当前平台接口只保存一个 _paste 回调:每个 UIPastComponent 挂载时都会调用 setPasteMethod,后注册的回调覆盖先注册的。多个输入区域同时使用组件时,只有最后挂载的组件能收到 pasted 文本。

如果多个区域都需要粘贴入口,推荐由应用级服务注册一个分发函数,按当前焦点输入框转发文本,或保证同一时间只挂载一个 UIPastComponent

9.7 编译成功但安装失败

常见原因包括:

  • HAP 未签名或使用了错误的 Profile;
  • 设备未加入调试设备列表;
  • 包名与签名 Profile 不匹配;
  • 安装包的 compatibleSdkVersion 高于设备 API;
  • 手机上已经安装了使用不同证书签名的同包名应用。

根据安装错误码区分签名、版本和包名冲突,再处理对应配置。

9.8 flutter create 不认识 ohos,或包名不合法

先执行 flutter --version,确认使用的是 OHOS 版工具链,环境配置回到第二节的社区链接核对。包名报错时,确认当前目录包含目标插件的 pubspec.yaml,并显式传入 --project-name ui_paste_component。生成后检查 diff,再补充 ArkTS 业务实现。

9.9 Git 依赖提示找不到分支或无权限

先检查 url 是否指向已包含 OHOS 适配的仓库:上游 GitHub 仓库的 master 分支尚不包含 ohos/ 目录,直接引用会构建失败。再确认 feat/ohos_ui_paste_component_0.0.6 已按第六节提交推送。私有仓库还需在本机配置 Git 认证。

9.10 改了本地 ArkTS,Demo 为什么没变化

先检查 example/pubspec.yaml:Git 依赖读取远程提交,不会自动读取本地插件改动。本地联调切回 path: ../;测试远程版本则先提交推送,再更新依赖并核对 pubspec.lockresolved-ref。原生代码变动后停止应用并重新构建运行,不能只做 Dart 热重载。

9.11 按钮渲染出来了,但没有文字标签

如果按钮位置只出现浅灰色圆角块而看不到 “Paste” 文字,通常是标签使用了 $r() 资源引用:资源管理器在 DynamicView 平台视图上下文中不能可靠解析,真机上按钮会渲染成空块。本例的 PasteComponentPlatformView.ets 使用固定字符串 'Paste' 作为标签,不依赖资源文件。排查时先确认组件代码没有改回 $r() 写法,再检查视图宽高是否被父级约束压缩为 0。

相关链接

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐