flutter_cached_uuid 鸿蒙 Flutter 插件适配实测,补齐 ArkTS UUID 生成与 preferences 持久化,Dart 层零改动,实现首次生成、缓存复用
开发工具: 华为云码道
DevEco Studio:
26.0.0(26.0.0.851)原始仓库: flappygod/flutter_cached_uuid(GitHub)
本文配套仓库: oh-flutter/flutter_cached_uuid(AtomGit)
flutter_cached_uuid 为应用提供一个「首次生成、之后每次返回同一个值」的 UUID:Android 侧优先由 ANDROID_ID 与 FINGERPRINT 派生,iOS 侧用 CFUUIDCreate 生成后写入 Keychain,两端都以「装一次、认一个」的方式对外提供标识。本文以 flutter_cached_uuid 1.0.10 为例,介绍源码准备、OHOS 平台实现、Example 宿主工程配置和真机运行验证。

这是一个插件库:Dart 层保持零改动,适配工作集中在 ArkTS 侧的 UUID 生成、preferences 持久化与上下文获取上。文中的代码以提交 1546638e3186f2332ab4a7b63c0f1cb38cf762ce 为参考,对应 TAG 1.0.10-ohos-1.0.0-beta.1。

一、插件简介与适配目标
应用经常需要一个「装一次就固定」的标识:埋点去重、灰度分桶、把本地配置和账号绑定、给客服提供排查用的设备代号。Android 有 Settings.Secure.ANDROID_ID,iOS 有 Keychain,OpenHarmony 上则没有可以直接照搬的等价物——这正是本库适配要解决的问题。
flutter_cached_uuid 对外只有一个方法:
FlutterCachedUuid.getUUID() -> Future<String?>
语义是「首次调用生成、之后每次返回同一个值」:
- 首次调用生成一个符合 RFC 4122 v4 的 UUID(32 位十六进制 + 4 个连字符,本库统一转成大写);
- 把它写入应用私有存储(Android 是
SharedPreferences,iOS 是 Keychain,本次适配是应用沙箱内的preferences); - 之后每次调用都读这份缓存,进程重启、应用重新打开都不变;
- 清除应用数据或卸载重装后,缓存消失,下次调用会生成新的值。
上游的工程结构是标准的 federated plugin 三层:门面类 FlutterCachedUuid、平台接口 FlutterCachedUuidPlatform、方法通道实现 MethodChannelFlutterCachedUuid,原生侧android/ 与 ios/ 各有一份实现。本次适配要做的是:
- 在
pubspec.yaml里声明ohos平台,让工具链生成注册代码; - 用 ArkTS 实现同名通道
flutter_cached_uuid的同名方法getUUID,并保证「生成一次、持久化、后续命中缓存」的语义与 Android / iOS 一致; - 生成可编译、可运行、可验证的
example/ohos宿主工程,在真机上跑通首次生成、连续调用、进程重启与清数据四条路径。
二、环境准备
环境搭建参考社区文档:Flutter OH 开发环境搭建,完成 Flutter OH SDK 安装、环境变量和 DevEco Studio 配置。
完成后,在宿主机终端执行以下命令,确认当前选中的是支持 OHOS 的 Flutter 工具链,并能发现目标设备:
flutter --version
flutter doctor -v
hdc list targets


工程使用的工具链和 SDK 配置如下:
| 项目 | 版本或配置 | 用途 |
|---|---|---|
| Flutter OHOS SDK | 3.47.7-ohos-1.0.0 | Flutter 编译与 OHOS 平台工具链 |
| Flutter 分支 | oh-3.47-dev | 对应开发分支 |
| Dart SDK | 3.13.3 | Dart 语言与包管理环境 |
| DevEco Studio | 26.0.0(26.0.0.851) | OHOS 应用工程开发套件 |
compatibleSdkVersion | 6.1.1(24) | 当前工程声明的最低兼容版本 |
targetSdkVersion | 6.1.1(24) | 应用面向的行为版本 |
| 测试设备 ROM | OpenHarmony-6.1.1.120(API 24) | 真机系统版本(TLR-AL00,nova 14) |
| 库版本 | 1.0.10 | pubspec.yaml 中的包版本 |
| 原生语言 | ArkTS | ohos/ 模块实现语言 |
| 插件产物 | HAR(ohos/) | 经 GeneratedPluginRegistrant.ets 自动注册 |
2.1 开发套件版本与工程中的 SDK 版本配置
6.1.1(24) 和工程配置涉及 OpenHarmony 版本号体系。在本文工程中,相关版本的含义与配置方式如下:
6.1.1(24)采用「版本(API)」写法,表示 OpenHarmony6.1.1版本、对应 API 24;compatibleSdkVersion与targetSdkVersion均为6.1.1(24):声明最低兼容 API 24,并以 API 24 为行为目标;- 本机安装的 DevEco Studio 自带 SDK 为
26.0.0(26),高于工程声明值,编译按本机 SDK 执行、安装兼容性按工程声明判断,两者并不冲突。

判断依据是设备的 hdc shell param get const.ohos.apiversion:本次测试机返回 24,与工程声明一致,安装与运行的版本门槛都满足。
三、从源码仓库开始准备适配工程
3.1 将上游源码同步到 AtomGit
适配已有第三方库时,先从 Pub 包信息或项目 README 确认上游地址、包名、版本和许可证。同步仓库时保留源码、许可证和提交历史,方便后续跟踪上游更新。
在 AtomGit 网页的新建仓库流程中,找到从已有仓库导入的入口,填写上游 Git 地址,选择自己有写权限的组织或个人空间,设置目标仓库名称后执行导入。完成后检查目标分支、pubspec.yaml、LICENSE 和提交历史是否完整,并记录适配起点的提交号。如果上游本身托管在 AtomGit,也可以通过 Fork 获得自己的工作仓库。
本例的上游为 flappygod/flutter_cached_uuid,包名 flutter_cached_uuid,pubspec.yaml 中的版本为 1.0.10,许可证为 BSD-3-Clause(LICENSE 开头是 Copyright 2023 flappy. All rights reserved.,随后是三段式条款)。适配起点的提交是 5a0eedc(上游最后一次功能提交,信息为 set synchroized false)。
仓库名与 Dart 包名一致,都是 flutter_cached_uuid,因此同步到 AtomGit 时保持原名即可,不必加 fluttertpc_ 前缀。
flutter_cached_uuid 现可从 AtomGit 获取。下面使用 AtomGit 地址拉取代码;需要提交修改时,使用自己有写权限的仓库或 Fork。
3.2 将代码拉取到宿主机
在安装了 Flutter OH 和 DevEco Studio 的开发电脑上打开终端,进入准备存放项目的目录,执行:
git clone https://atomgit.com/oh-flutter/flutter_cached_uuid.git
cd flutter_cached_uuid
pwd
ls
git remote -v
git status --short --branch
git rev-parse HEAD
git clone 会创建 flutter_cached_uuid/ 目录;cd 后的位置就是下文所说的仓库根目录,这里应能看到 pubspec.yaml、lib/、ohos/、example/,以及上游保留的 android/ 与 ios/。
需要使用与本文相同的代码版本时,在没有未提交修改的仓库中切换到以下提交:
git switch --detach 1546638e3186f2332ab4a7b63c0f1cb38cf762ce
适配其他插件时,使用该插件对应版本的 tag 或 commit 作为分支起点。

图 1:在宿主机终端输入 AtomGit 仓库拉取命令。
3.3 在仓库根目录确认分支与版本标记
本库没有为 OHOS 适配单独开特性分支,适配提交直接落在 main 上,版本通过 TAG 标记。TAG 命名规则为 原库版本-ohos-版本号-beta.x,本库使用 1.0.10-ohos-1.0.0-beta.1。在 flutter_cached_uuid/ 根目录执行:
git branch --show-current
git tag --list
git log --oneline -3
git status --short --branch
git branch --show-current 用于确认当前所在分支,git tag --list 用于确认版本标记是否已推送。向接收仓库提交时按其对分支与 TAG 的要求执行。

图 2:在 flutter_cached_uuid 仓库根目录确认分支与 TAG。
图 2 同时暴露了一个需要注意的状态:工作区里还有两处未提交改动——example/analysis_options.yaml 与 example/pubspec.lock。它们不影响库本体,但提交前要么一并提交、要么还原,避免留下「文档说干净、实际有残留」的错觉(处理方式见 6.3)。
3.4 补全 ohos 平台与 example 宿主工程
上游已经按插件形式声明了 android 与 ios 平台,只是缺少 ohos。补全方式是在仓库根目录执行插件模板生成,让它同时产出 ohos/ 模块与 example/ohos/ 宿主工程:
flutter create . --template=plugin --platforms=ohos
git status --short
git diff -- pubspec.yaml
--template=plugin匹配本库的插件身份,生成 HAR 模块骨架;--platforms=ohos指定只补全 OHOS 平台,不影响已有的android/、ios/实现;- 最后的
.表示在当前插件根目录内补全,不是另建一层目录。
生成后务必用 diff 检查:模板会连带注入与 OHOS 无关的内容,例如 .kts 构建脚本、*.iml、local.properties,还可能改写 analysis_options.yaml。本次适配保留了已有 API、其他平台注册项及依赖配置,只保留 ohos/ 与 example/ohos/ 相关产物。不同 Flutter OH 版本生成的模板可能略有差异。
pubspec.yaml 的净变化只有 2 行——在 flutter: plugin: platforms 下追加 ohos 声明:
flutter:
plugin:
platforms:
android:
package: com.flappy.flutter_cached_uuid
pluginClass: FlutterCachedUuidPlugin
ios:
pluginClass: FlutterCachedUuidPlugin
ohos:
pluginClass: FlutterCachedUuidPlugin
pluginClass 必须与 ArkTS 侧 getUniqueClassName() 的返回值一致(都是 FlutterCachedUuidPlugin),否则插件注册表找不到类。
顺便说明一个与同类老库不同的地方:本库上游的 SDK 约束已经是 sdk: '>=3.2.6 <4.0.0'、flutter: '>=3.3.0',本身就在 Dart 3 的范围内,适配过程不需要放宽 SDK 上界。

图 3:在仓库根目录输入 OHOS 平台补全命令。
3.5 适配后的项目目录
适配后的关键目录如下:
flutter_cached_uuid/
├── lib/
│ ├── flutter_cached_uuid.dart
│ ├── flutter_cached_uuid_method_channel.dart
│ └── flutter_cached_uuid_platform_interface.dart
├── ohos/
│ ├── index.ets
│ ├── oh-package.json5
│ ├── build-profile.json5
│ ├── hvigorfile.ts
│ └── src/main/
│ ├── module.json5
│ └── ets/components/plugin/FlutterCachedUuidPlugin.ets
├── example/
│ ├── lib/main.dart
│ └── ohos/
│ ├── build-profile.json5
│ ├── oh-package.json5
│ └── entry/src/main/
│ ├── ets/entryability/EntryAbility.ets
│ ├── ets/plugins/GeneratedPluginRegistrant.ets
│ ├── module.json5
│ └── resources/
├── test/
├── README.OpenHarmony_CN.md
├── CHANGELOG.OpenHarmony.md
└── pubspec.yaml
项目根目录如下,其中包含 ohos/、example/ohos/,以及 OpenHarmony 中英文说明和变更记录:

图 4:适配后的 flutter_cached_uuid 项目根目录。
| 文件 | 主要职责 |
|---|---|
lib/flutter_cached_uuid.dart | 门面类,业务侧唯一入口 FlutterCachedUuid.getUUID() |
lib/flutter_cached_uuid_platform_interface.dart | 平台接口,允许注入自定义实现 |
lib/flutter_cached_uuid_method_channel.dart | 方法通道实现,固定通道名 flutter_cached_uuid |
ohos/src/main/ets/components/plugin/FlutterCachedUuidPlugin.ets | ArkTS 插件:注册、上下文获取、生成与持久化 |
ohos/index.ets | 导出插件类,供宿主工程引用 |
example/lib/main.dart | 示例页:当前 UUID、调用历史、稳定性结论、耗时 |
example/ohos/build-profile.json5 | 声明 product、SDK 版本与签名配置 |
README.OpenHarmony_CN.md / README.OpenHarmony.md | OpenHarmony 中英文说明 |
CHANGELOG.OpenHarmony.md | 鸿蒙化变更记录 |
四、Dart 实现分析
本库的 Dart 层只有 3 个文件、共 53 行,典型的插件三层结构,本次适配零改动。三个文件全文如下:
// lib/flutter_cached_uuid.dart
import 'flutter_cached_uuid_platform_interface.dart';
class FlutterCachedUuid {
static Future<String?> getUUID() {
return FlutterCachedUuidPlatform.instance.getUUID();
}
}
// lib/flutter_cached_uuid_method_channel.dart
class MethodChannelFlutterCachedUuid extends FlutterCachedUuidPlatform {
final methodChannel = const MethodChannel('flutter_cached_uuid');
Future<String?> getUUID() async {
final version = await methodChannel.invokeMethod<String>('getUUID');
return version;
}
}
// lib/flutter_cached_uuid_platform_interface.dart
abstract class FlutterCachedUuidPlatform extends PlatformInterface {
FlutterCachedUuidPlatform() : super(token: _token);
static final Object _token = Object();
static FlutterCachedUuidPlatform _instance = MethodChannelFlutterCachedUuid();
static FlutterCachedUuidPlatform get instance => _instance;
static set instance(FlutterCachedUuidPlatform instance) {
PlatformInterface.verifyToken(instance, _token);
_instance = instance;
}
Future<String?> getUUID() {
throw UnimplementedError('getUUID() has not been implemented.');
}
}
4.1 跨端架构与调用时序
Dart 侧只做一件事:把 getUUID 原封不动地交给平台实现。真正决定「返回什么」的是原生侧。
对适配工作的直接含义是:通道名和方法名在 Dart 侧已经钉死,ArkTS 侧只能照着 flutter_cached_uuid / getUUID 这两个名字实现,改任何一个都会立刻变成 MissingPluginException。
4.1.1 一次 getUUID 的完整时序
时序图里有两个容易被忽略的点,也是本库适配的核心:上下文从哪来、什么时候必须 flush,分别写在 5.2 与 5.3。
4.2 三层结构的意义
PlatformInterface 这一层看着多余,实际是这个库能在四个平台共用一套 Dart 代码的原因:
| 层 | 作用 | 适配时是否需要改 |
|---|---|---|
门面 FlutterCachedUuid | 静态方法,业务只认这一层 | 否 |
平台接口 FlutterCachedUuidPlatform | 定义契约、持有默认实现、支持替换 | 否 |
通道实现 MethodChannelFlutterCachedUuid | 真正发通道消息 | 否(通道本身与平台无关) |
PlatformInterface.verifyToken 保证只有本库创建的实例才能被 set instance 替换,避免业务侧误用别的实现——这也是单元测试能注入 mock 的前提(见 8.1)。
4.3 公开 API
| 名称 | 描述 | 类型 | 返回值 |
|---|---|---|---|
FlutterCachedUuid.getUUID() | 获取缓存过的 UUID,首次调用生成并持久化 | 静态方法 | Future<String?> |
FlutterCachedUuidPlatform.instance | 获取 / 设置平台实现实例 | 静态属性 | FlutterCachedUuidPlatform |
FlutterCachedUuidPlatform.getUUID() | 平台实现需覆写的方法 | 抽象方法 | Future<String?> |
返回值声明为 Future<String?> 而不是 Future<String>:通道方法在原生侧返回非字符串时(例如插件异常路径),Dart 侧可能拿到 null。业务侧建议按 7.3 的写法处理空值。
4.4 为什么 Dart 层可以零改动
因为「缓存」这件事天然属于平台侧:Android 的 SharedPreferences、iOS 的 Keychain、OpenHarmony 的 preferences 都是各自的持久化设施,Dart 侧既拿不到也不该拿。Dart 层只负责发一条 getUUID 请求,把「生成与存储」全部留给原生,这正是本库在鸿蒙上能不改一行 Dart 就落地的原因。
对比上一类「纯 Dart 库」(例如十六进制色值解析),那种库的适配是「宿主工程 + 文档」,而本库必须写真实的 ArkTS 实现——两者的适配工作量不在一个量级,判断依据就是 Dart 层有没有 MethodChannel。
五、ArkTS 平台实现
5.1 插件注册
pubspec.yaml 声明 ohos 平台后,Flutter 工具会在构建时为宿主工程生成注册代码。本库适配后的 example/ohos/entry/src/main/ets/plugins/GeneratedPluginRegistrant.ets 内容如下:
import { FlutterEngine, Log } from '@ohos/flutter_ohos';
import IntegrationTestPlugin from 'integration_test';
import FlutterCachedUuidPlugin from 'flutter_cached_uuid';
export class GeneratedPluginRegistrant {
static registerWith(flutterEngine: FlutterEngine) {
try {
flutterEngine.getPlugins()?.add(new IntegrationTestPlugin());
flutterEngine.getPlugins()?.add(new FlutterCachedUuidPlugin());
} catch (e) {
Log.e(TAG, "Tried to register plugins with FlutterEngine failed.");
Log.e(TAG, "Received exception while registering", e);
}
}
}
GeneratedPluginRegistrant.ets 是生成文件,通常不应手工编辑,下次构建可能覆盖它;注册是否生效可以直接看 import 语句里有没有本库条目。
5.2 上下文从哪来:AbilityAware 而不是只要 Ability
preferences 的读写必须拿到 Context。Flutter OHOS 的插件框架给了两个来源,本实现两个都用,并按「Ability 优先、应用级兜底」的顺序取:
export default class FlutterCachedUuidPlugin
implements FlutterPlugin, MethodCallHandler, AbilityAware {
private applicationContext: common.Context | null = null;
private abilityContext: common.UIAbilityContext | null = null;
onAttachedToEngine(binding: FlutterPluginBinding): void {
this.applicationContext = binding.getApplicationContext();
this.channel = new MethodChannel(binding.getBinaryMessenger(), CHANNEL_NAME);
this.channel.setMethodCallHandler(this);
hilog.info(DOMAIN, TAG, 'attached to engine, channel [%{public}s] registered', CHANNEL_NAME);
}
onAttachedToAbility(binding: AbilityPluginBinding): void {
this.abilityContext = binding.getAbility().context;
hilog.info(DOMAIN, TAG, 'attached to ability');
}
onDetachedFromAbility(): void {
// 释放引用避免泄漏,后续调用自动回退到应用级上下文
this.abilityContext = null;
}
}
| 上下文 | 来源 | 何时可用 | 用途 |
|---|---|---|---|
applicationContext | FlutterPluginBinding.getApplicationContext() | 插件挂载到引擎时即获得,始终可用 | 兜底,保证没有 Ability 时也能读写 |
abilityContext | AbilityPluginBinding.getAbility().context | UIAbility 附着后才有 | 首选,语义对齐 Android 的 onAttachedToActivity |
如果只实现 FlutterPlugin 不实现 AbilityAware,在「引擎已创建但 UIAbility 尚未附着」的时间窗里调用 getUUID 就会拿不到上下文——本次实现用 applicationContext 兜住了这种情况,代码里对「两者都为空」也留了明确的错误返回,而不是抛空指针。
5.3 getOrCreateUUID:读缓存、未命中则生成并落盘
真正实现「缓存」语义的是这一段:
private async getOrCreateUUID(context: common.Context): Promise<string> {
const store: preferences.Preferences = await preferences.getPreferences(context, PREFERENCES_NAME);
const cachedValue: preferences.ValueType = await store.get(KEY_CACHED_UUID, NO_CACHED_UUID);
if (typeof cachedValue == 'string' && (cachedValue as string).length > 0) {
// 命中缓存:直接返回首次生成的值
return cachedValue as string;
}
// 未命中:生成 RFC 4122 v4 UUID,并转大写与 Android / iOS 的返回格式保持一致
const uuid: string = util.generateRandomUUID(true).toUpperCase();
await store.put(KEY_CACHED_UUID, uuid);
// 主动落盘,避免进程被杀死时缓存丢失(等价于 Android 的 editor.apply())
await store.flush();
hilog.info(DOMAIN, TAG, 'generated a new UUID and cached it into preferences');
return uuid;
}
几个刻意的对齐点:
| 细节 | Android / iOS | 本次 ArkTS 实现 |
|---|---|---|
| 生成方式 | Android 优先设备标识派生,iOS 用 CFUUIDCreate | util.generateRandomUUID(true),与 iOS 同思路 |
| 大小写 | 两端口径不一致,本库对外统一大写 | .toUpperCase() |
| 缓存位置 | SharedPreferences / Keychain | 应用沙箱内 preferences/flutter_cached_uuid 文件 |
| 何时落盘 | editor.apply() / Keychain 写入即持久 | 显式 await store.flush() |
| 空值判定 | 未缓存时为空 | get 的默认值传 '',长度为 0 视为未命中 |
flush() 这一步不能省。preferences 的 put 先改内存,flush 才写文件;省掉它的话,应用被系统回收时刚生成的 UUID 可能还没落盘,下次启动就变成「重新生成」——表现是「同一台机器每次冷启动都换 ID」,而看代码又找不出问题。
5.4 为什么不用 deviceInfo.udid
OpenHarmony 上确实存在「设备唯一标识」,但对普通应用不可用:
| 字段 | 需要的权限 | 三方应用能否申请 |
|---|---|---|
deviceInfo.udid / deviceInfo.serial | ohos.permission.sec.ACCESS_UDID | 否,仅系统应用与企业应用 |
deviceInfo.distributionOSVersion 等版本字段 | 无 | 可以 |
申请不到就意味着「拿设备 SN 当 ID」这条路对三方应用是堵死的。因此本实现选择了与 iOS 相同的策略:本地随机生成一次、持久化缓存,不采集任何硬件信息,也不需要任何权限。
这样做的代价与收益都写在下表里,接入前应对齐业务预期:
| 平台 | 缓存位置 | 生成方式 | 卸载重装后 | 同设备不同应用 |
|---|---|---|---|---|
| Android | SharedPreferences | 优先由设备标识派生 | 变化 | 可能相同(同源派生) |
| iOS | Keychain | 随机生成(CFUUIDCreate) | 通常不变(Keychain 数据保留) | 不同 |
| OpenHarmony | 应用沙箱 preferences | 随机生成并 flush 落盘 | 变化(随应用数据清除) | 不同 |
也就是说:鸿蒙侧的 UUID 是「应用安装维度」的标识,不是「设备维度」的标识。如果业务需要跨应用、跨重装都稳定的标识,应当改用账号体系或系统级授权标识,而不是本库——这一点在 README.OpenHarmony_CN.md 的「其他」小节也写明了。
5.5 权限
5.5.1 插件侧:HAR 无权限
ohos/src/main/module.json5 是 HAR 模块声明,内容只有模块名、类型和设备类型,没有 requestPermissions 配置段:
{
"module": {
"name": "flutter_cached_uuid",
"type": "har",
"deviceTypes": ["default", "tablet"]
}
}
util.generateRandomUUID 与 @ohos.data.preferences 都属于免权限接口,读写的也只是应用自己的沙箱目录,因此本插件不声明、也不需要任何权限。
5.5.2 应用 entry 的权限
example/ohos/entry/src/main/module.json5 中只有模板自带的 ohos.permission.INTERNET:
"requestPermissions": [
{"name" : "ohos.permission.INTERNET"},
]
这一条是宿主工程模板自带的,与本库的功能无关,保留或删除都不影响运行。除此之外整个工程没有任何权限声明,也不涉及运行时授权弹窗。
六、补全交付文件并提交适配分支
6.1 除代码外还要补全哪些文件
代码适配完成后,还需要整理安装说明、接口文档、版本记录和开源信息。接收仓库有专用模板时,按其格式填写:
| 文件 | 应写清楚的内容 |
|---|---|
README.OpenSource | 上游名称、源码地址、适配版本或提交、版权及许可证信息;按仓库模板列出第三方依赖 |
README.md | 原项目说明、OHOS 支持入口、配套 Demo 和文档链接;保留上游信息 |
README.OpenHarmony_CN.md | 简介、安装方式、版本对应关系、环境约束、权限、接口表、示例、已验证范围和遗留问题 |
README.OpenHarmony.md | 与中文说明对应的英文文档 |
CHANGELOG.OpenHarmony.md | OHOS 新增能力、适配版本、兼容限制与测试范围 |
LICENSE / NOTICE | 保留上游 LICENSE 原文;上游未声明协议时不要擅自添加 |
example/README.md | 依赖方式、运行目录、签名、操作步骤与效果图 |
pubspec.yaml | 核对包名、版本、SDK 约束和依赖 |
本仓库已提供 README.OpenHarmony_CN.md、README.OpenHarmony.md 与 CHANGELOG.OpenHarmony.md(其中 CHANGELOG.OpenHarmony.md 把「不用 UDID 的原因」也写进去了,这一点值得保留)。发布前有三处需要核对统一:
- 两份 README 的下载地址目前写的是
atomgit.com/CPF-Flutter/fluttertpc_flutter_cached_uuid,组织名和仓库名都与实际托管位置atomgit.com/oh-flutter/flutter_cached_uuid不一致;接入时以本文 7.2 的配置为准,或先统一文档地址; - README 的 TAG 表中分支列写的是
br_v1.0.10_ohos,实际分支为main,发布前统一; - 许可证表述不一致:上游
LICENSE是 BSD-3-Clause,而两份 README 的「开源协议」小节与ohos/oh-package.json5的license字段都写着Apache-2.0。分发时以LICENSE原文为准,同时把这两处描述改成BSD-3-Clause,避免下游误判合规义务。
6.2 提交前检查
提交前先完成第八节的 example 和真机测试,再从根目录检查改动:
git branch --show-current
git diff --check
git status --short
git diff --stat
git diff
检查 diff 中的依赖变化,移除本机路径及签名信息,并使文档中的版本和分支与提交内容一致。
签名凭据必须清理(本仓库当前不满足): example/ohos/build-profile.json5 里 signingConfigs 带着完整的本机签名材料——certpath / storeFile / profile 指向 C:\Users\<用户名>\.ohos\config\...,keyPassword 与 storePassword 是加密后的口令串,而这份文件已经随适配提交 1546638 进入了仓库历史(git show 1546638:example/ohos/build-profile.json5 能直接看到)。正确做法是提交前把它还原成空数组:
"signingConfigs": [],
调试签名材料只应留在开发者本机(~/.ohos/config 下的自动签名材料与个人账号绑定,不适合进入公开仓库)。已经提交的记录需要按接收仓库的安全要求决定是否清理历史;后续提交务必先还原。
6.3 提交并推送到 AtomGit
文档和代码整理完成后,在根目录暂存并提交。文件名按项目实际情况调整:
git add lib pubspec.yaml ohos example test
git add README.OpenHarmony_CN.md README.OpenHarmony.md
git add CHANGELOG.OpenHarmony.md
git diff --cached --check
git diff --cached --stat
git commit -m "feat(ohos): add OpenHarmony platform support"
git remote -v
git branch --show-current
git push -u origin main
git tag 1.0.10-ohos-1.0.0-beta.1
git push origin 1.0.10-ohos-1.0.0-beta.1
本例的适配已合并为一次提交 1546638,推送在 AtomGit 的 main 分支上,TAG 1.0.10-ohos-1.0.0-beta.1 已随提交推送(见图 2)。
图 2 里那两处未提交改动,处理建议如下:
# example/analysis_options.yaml:新增了 analyzer.exclude(build/**、android/**、ios/**)
# 本库已经补了 ohos/,建议同时排除 ohos/**,否则 analyze 会扫到宿主工程
git diff example/analysis_options.yaml
# example/pubspec.lock:随依赖解析更新,随 example 一起提交即可
git add example/analysis_options.yaml example/pubspec.lock
git commit -m "chore(example): exclude build output from analysis and refresh lockfile"
推送后在 AtomGit 发起合并请求,说明上游来源和版本、适配范围(ArkTS 生成 + preferences 持久化,Dart 层零改动)、依赖变化(仅 pubspec 新增 ohos 平台声明)、测试环境、操作结果、已知限制,并附 Demo 运行图。目标分支和评审流程以接收仓库要求为准。
七、使用根目录 example 演示接入
仓库自带 example/,可以直接用来调试和体验「首次生成、之后命中缓存」的完整链路。
7.1 本地适配时使用路径依赖
当前 example/pubspec.yaml 的依赖是:
dependencies:
flutter:
sdk: flutter
flutter_cached_uuid:
path: ../
../ 相对于 example/pubspec.yaml 指向仓库根目录,修改根目录的库代码后可直接联调。
7.2 通过 AtomGit 引入库
业务应用通过 AtomGit 引入时,将 flutter_cached_uuid 的 path 配置替换为下面的 Git 依赖。这里固定到本文使用的 TAG:
dependencies:
flutter:
sdk: flutter
flutter_cached_uuid:
git:
url: https://atomgit.com/oh-flutter/flutter_cached_uuid.git
ref: 1.0.10-ohos-1.0.0-beta.1
使用自己的适配版本时,先推送分支,再将 url 改为对应仓库,ref 改为分支名。正式发布后可固定到 tag 或 commit。
从仓库根目录执行:
cd example
flutter pub get
flutter pub deps
检查 example/pubspec.lock 中 flutter_cached_uuid 的来源为 git,并核对 url、ref 和 resolved-ref。同时检查没有 dependency_overrides 或 pubspec_overrides.yaml 将其覆盖回本地依赖,确认应用使用的是 Git 依赖。
7.3 调用接口获取 UUID
业务侧的典型用法只有一行,但要处理好空值与异常:
import 'package:flutter/services.dart';
import 'package:flutter_cached_uuid/flutter_cached_uuid.dart';
Future<String> ensureDeviceId() async {
try {
final String? uuid = await FlutterCachedUuid.getUUID();
if (uuid == null || uuid.isEmpty) {
// 原生侧返回了非字符串:按「无标识」降级,不要当成崩溃
return '';
}
return uuid;
} on PlatformException catch (e) {
// 原生侧抛错时 code 为 CACHED_UUID_ERROR,message 带具体原因
print('getUUID failed: ${e.code} / ${e.message}');
return '';
} on MissingPluginException {
// 当前平台没有注册插件(例如 Web / 未适配的平台)
return '';
}
}
调用时机建议放在应用启动后的第一屏,而不是 main() 的最前面——preferences 依赖 Ability 上下文,太早调用会走到应用级上下文兜底或直接报「no context available」。
7.4 业务侧的三条注意事项
- 不要把它当设备唯一标识:同设备不同应用、卸载重装后都会变(见 5.4 的对照表);
- 不要高频调用后自己再缓存一层:库本身已经缓存,同进程内每次调用只花 1–2 ms(见 8.6 的实测耗时),再套一层容易在清数据后与库内真实值不一致;
- 不要用它做安全凭据:UUID 是随机串,可被清数据重置,不具备防伪能力,只能做「同一安装实例的关联键」。
八、验证、构建与鸿蒙设备运行效果
8.1 分别验证库与 example
本仓库包含单元测试 test/flutter_cached_uuid_method_channel_test.dart,用 setMockMethodCallHandler 把通道打桩成固定返回 '42',验证 MethodChannelFlutterCachedUuid.getUUID() 的透传行为。从仓库根目录执行:
flutter pub get
flutter test
flutter analyze
cd example
flutter pub get
flutter analyze
一个需要留意的历史遗留:这个用例的函数名还叫 test('getPlatformVersion', ...),是从插件模板复制后没改名,实际断言的是 getUUID。它不影响结果,但会让第一次读代码的人误以为漏测了方法,建议顺手改名。
单元测试验证的是 Dart 层透传,所有平台一致;生成、持久化与上下文获取只能在鸿蒙设备上确认。
8.2 确认设备连接
hdc list targets
flutter devices
设备首次连接电脑时,需要在手机端确认调试授权。列表为空时,检查 USB 连接、调试模式和电脑授权。
8.3 配置签名
真机安装的 HAP 需要有效签名。本仓库当前提交里的 signingConfigs 是别人的本机材料,不能直接用(见 6.2),请替换为自己的调试签名:
- 用 DevEco Studio 打开
example/ohos,不是仓库根目录; - 等待工程 Sync 成功,确认 Project 视图中存在
entry模块; - 打开 File > Project Structure > Signing Configs;
- 勾选 Automatically generate signature;
- 确认设备、应用包名、证书和 Profile 匹配(调试 Profile 与
bundleName: com.flappy.flutter_cached_uuid_example强绑定); - 再回到终端执行构建或安装。
8.4 构建与安装
在 example/ 目录执行 flutter run -d <device-id> 可以直接构建运行。仅构建 HAP 时:
flutter build hap --debug
Windows 环境下该命令可能因 DevEco Studio 26 的 ohpm.bat 批处理递归缺陷中断(见 9.12),可改用工程内等价的构建命令:
cd example/ohos
hvigorw assembleHap -p product=default -p buildMode=debug --no-daemon
本次验证用的正是后者,构建结尾输出:
> hvigor Finished :entry:assembleHap... after 1 ms
> hvigor BUILD SUCCESSFUL in 15 s 273 ms
50 tasks in total: 24 executed, 26 up-to-date
产物位于 example/ohos/entry/build/default/outputs/default/entry-default-signed.hap(另有 entry-default-unsigned.hap)。真机安装选择已签名产物:
hdc install -r entry-default-signed.hap
hdc shell aa start -a EntryAbility -b com.flappy.flutter_cached_uuid_example
这次执行里编译与打包任务多为 UP-TO-DATE(源码自 10-19 起未再改动),构建结论为 BUILD SUCCESSFUL,因此产物与当前源码一一对应。pack.info 中的 apiVersion 为 {"compatible": 24, "releaseType": "Release", "target": 24},与测试设备(API 24)一致。
8.5 在设备上测试缓存语义
example 页面按「当前值 / 操作 / 稳定性 / 历史」分区,逐项核对:
- 启动应用:
Cached UUID卡片显示Tap "Get UUID" below,稳定性格显示Not enough samples,历史区为No call yet.(图 5); - 点按 Get UUID:卡片出现大写 UUID,历史区新增第 1 条并给出耗时(首次调用含生成与落盘,通常 130–150 ms,图 5);
- 点按 Call x3:连续调用 3 次,稳定性结论变为
Stable: same UUID returned,副标题为Calls: 4 · Distinct values: 1,历史区第 2–4 条的耗时降到 1–2 ms(图 6 左); - 退出应用后重新打开(或
hdc shell aa force-stop后重新aa start):再点 Get UUID,返回值与重启前完全相同——这是「缓存」生效的直接证据(图 6 右); - 点按历史条目右侧的复制图标:底部出现
UUID copied提示,说明 clipboard 在鸿蒙上同样可用(图 7 左); - 清除应用数据(
hdc shell bm clean -n com.flappy.flutter_cached_uuid_example -d)后重新启动再取一次:返回值变为新的 UUID——与 5.4 表格里「随应用数据清除而变化」一致(图 7 中); - 对照 About 区两条说明(
Generated once and persisted、No permission required),确认运行环境与文档一致(图 7 右)。
本文已在真机完成上述全部 7 项,对应 7 张实拍截图(见 8.6)。
8.6 鸿蒙设备运行效果
首次调用与连续调用,是这套语义最核心的两张图:
| 启动初始状态 | 首次调用 |
|---|---|
卡片提示 Tap "Get UUID" below,Calls: 0 · Distinct values: 0,历史区 No call yet. | 生成 10DF75F5-68D3-4692-BB24-5F13C1B74BBF,历史第 1 条耗时 145 ms(含生成与 flush) |
连续调用与进程重启后的返回值:
| 连续调用 x3(同一进程) | 强制退出并重启后 |
|---|---|
Stable: same UUID returned,Calls: 4 · Distinct values: 1,第 2–4 次耗时 1 ms / 1 ms / 2 ms | 进程重建、历史清空后再取一次,返回值仍是 10DF75F5-…-5F13C1B74BBF,耗时 134 ms(冷启动首次调用需打开 preferences 文件) |
复制提示、清数据后重新生成,以及 About 区的行为声明:
| 复制提示 | 清除数据后 | About 区 |
|---|---|---|
底部 UUID copied,说明剪贴板能力可用 | bm clean -d 后重新生成 4A2E670F-EE76-4972-9D8B-84F841F96D85,耗时 151 ms | 明确写着「生成一次即持久化」,且「无需 ACCESS_UDID 权限」 |
界面之外,插件的注册、命中与生成都可以从 hilog 独立验证(TAG FlutterCachedUuidPlugin,下面按 TAG 与消息裁剪了模块前缀列):
# 进程 9413:首次调用,生成了新值并落盘
16:08:28.057 Adding plugin: FlutterCachedUuidPlugin
16:08:28.057 attached to engine, channel [flutter_cached_uuid] registered
16:08:28.057 attached to ability
16:08:48.534 onMethodCall: getUUID
16:08:48.677 generated a new UUID and cached it into preferences
16:08:48.677 getUUID succeeded
# 同一进程内连续 3 次:只有 succeeded,没有 generated,说明走的是缓存
16:09:00.756 onMethodCall: getUUID
16:09:00.757 getUUID succeeded
16:09:00.757 onMethodCall: getUUID
16:09:00.758 getUUID succeeded
16:09:00.759 onMethodCall: getUUID
16:09:00.759 getUUID succeeded
# 进程 9523:强制退出后重启,直接命中缓存
16:09:18.120 attached to engine, channel [flutter_cached_uuid] registered
16:09:24.407 onMethodCall: getUUID
16:09:24.407 getUUID succeeded
# 进程 9999:bm clean -d 清数据后重新生成
16:10:03.968 onMethodCall: getUUID
16:10:04.116 generated a new UUID and cached it into preferences
16:10:04.116 getUUID succeeded
落盘结果也可以直接在设备上查到,preferences 把键值写成了 XML:
$ hdc shell ls -l /data/app/el2/100/base/com.flappy.flutter_cached_uuid_example/haps/entry/preferences
-rw-rw---- 1 20020242 20020242 160 2026-10-22 16:10 flutter_cached_uuid
-rw-rw---- 1 20020242 20020242 0 2026-10-22 16:10 flutter_cached_uuid.lock
$ hdc shell cat /data/app/el2/100/base/com.flappy.flutter_cached_uuid_example/haps/entry/preferences/flutter_cached_uuid
<?xml version="1.0" encoding="UTF-8"?>
<preferences version="1.0"><string key="flutter_cached_uuid">4A2E670F-EE76-4972-9D8B-84F841F96D85</string></preferences>
文件里的值与图 7 中清数据后重新生成的值完全一致,说明「Dart 拿到什么」和「磁盘上存了什么」是同一条链路的结果,不存在中间层再加工。
需要说明的是:getUUID 返回的字符串取决于首次生成时的随机值,各设备、各次安装都不同;而通道名、方法名与「生成一次、之后命中」的行为与设备硬件无关,在所有支持 Flutter 的 OpenHarmony 设备上一致。
九、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 的组件已经下载完整;
- 检查 Flutter 和 DevEco Studio 使用的 SDK 路径是否一致;
- 避免误用不完整的 SDK 目录;
- 确认 SDK 根目录下存在
toolchains、ets、js、native、previewer; - 执行
flutter config --ohos-sdk <正确路径>; - 重新执行
flutter doctor -v和 DevEco Studio Sync。
因为 Sync 和 Compile 首先读取宿主机本地 SDK。手机 API 版本只在部署、安装和运行时参与兼容判断。即使完全不连接手机,本地 SDK 不完整时也会得到相同错误。
当前工程的 compatibleSdkVersion 是 6.1.1(24)(API 24),因此 API 24 设备在安装版本门槛上是满足的。
9.2 DevEco Studio 中看不到 entry 模块
可安装应用的 entry 模块位于 example/ohos/entry。
请直接使用 DevEco Studio 打开:
flutter_cached_uuid/example/ohos
如果仍看不到 entry,先解决 SDK Sync 错误,再检查 example/ohos/build-profile.json5 的 modules 是否包含 ./entry。同步失败时,Project Structure 无法正确解析模块,签名界面也可能不显示 entry。
9.3 仓库里的签名配置能直接用吗
不能。本仓库 example/ohos/build-profile.json5 里的 certpath / storeFile / profile 指向的是提交者本机的 ~/.ohos/config 路径,口令也是与本机证书绑定的加密串。换一台机器构建时,这些路径不存在,签名会直接失败;即使路径恰好存在,证书与包名、Profile 也不匹配。
请按 8.3 用自动签名为自己的环境重新生成,并把这份文件还原成 "signingConfigs": [] 后再提交(见 6.2)。
9.4 安装报 code:9568297 older sdk version in the device
工程声明的 compatibleSdkVersion 高于设备 API 版本时会出现这个报错。处理方式是把 example/ohos/build-profile.json5 中 product 的两个版本值下调到设备支持的 API:
"compatibleSdkVersion": "6.1.1(24)",
"targetSdkVersion": "6.1.1(24)",
判断依据是设备 param get const.ohos.apiversion 的返回值——工程声明的 API 版本不能高于目标设备的 API 版本。
9.5 安装报 no signature file 或 install version downgrade
真机安装的另外两个常见错误码:
| 报错 | 原因 | 处理 |
|---|---|---|
code:9568320 no signature file | 装了未签名 HAP | 使用 entry-default-signed.hap 安装 |
code:9568263 install version downgrade | 目标包名已被更高 versionCode 的应用占用 | 卸载旧应用、换包名或提升 versionCode |
9.6 清除应用数据 / 卸载重装后 UUID 变了,是不是 bug
不是,这是设计如此。缓存写在应用沙箱的 preferences 里,清数据或卸载会一并删除(见 5.4 的对照表,图 7 中就是清数据后重新生成的新值)。如果需要「重装后不变」,只能用账号体系或系统级授权标识,本库在鸿蒙上做不到,在 Android 上同样做不到。
9.7 getUUID 返回 null 或抛 MissingPluginException
按可能性排序:
- 插件没注册:检查
GeneratedPluginRegistrant.ets里有没有new FlutterCachedUuidPlugin(),以及pubspec.yaml的ohos.pluginClass是否为FlutterCachedUuidPlugin; - 通道名被改:Dart 侧固定是
flutter_cached_uuid,ArkTS 侧CHANNEL_NAME必须完全一致; - 在非鸿蒙平台调用:Web、Windows 等没有对应实现,
MissingPluginException是预期行为,按 7.3 的方式捕获降级; getUUID被提前调用:Ability 还没附着、应用级上下文也没拿到时会返回CACHED_UUID_ERROR,提示No context available to access preferences,把调用时机放到首屏之后即可(见 7.3)。
9.8 冷启动第一次调用要 130 ms 左右,正常吗
正常,而且这不是生成 UUID 的开销。preferences.getPreferences 每次进程首次调用都要打开(必要时创建)沙箱里的存储文件,这部分耗时占了大头;同一进程内的后续调用是 1–2 ms(见图 6 左)。首次「生成 + put + flush」比「仅命中」多花十几毫秒(145 ms 对 134 ms),量级差别不大。
如果业务对启动耗时敏感,可以在首屏渲染完成后再异步取,或把结果保存在内存里复用。
9.9 flutter create 不认识 ohos
先执行 flutter --version,确认使用的是 OHOS 版工具链,环境配置回到第二节的社区链接核对。注意 --template=plugin 只应对库根目录执行一次;对 example/ 或其他目录重复执行会引入多余的模板文件,生成后务必 diff 检查并清理。
9.10 AtomGit 依赖提示找不到分支或无权限
先检查 URL 是否指向已同步的目标仓库(https://atomgit.com/oh-flutter/flutter_cached_uuid.git),再确认 TAG 1.0.10-ohos-1.0.0-beta.1 已推送。仓库 README 中目前的下载地址(CPF-Flutter/fluttertpc_flutter_cached_uuid)与实际不一致(见 6.1),接入时以本文 7.2 的配置为准,或先统一文档地址。私有仓库还需在本机配置 Git 认证。
9.11 改了本地库代码,Demo 为什么没变化
先检查 example/pubspec.yaml:Git 依赖读取远程提交,不会自动读取本地库改动。本地联调切回 path: ../。Dart 文件变更用热重载/热重启即可生效;但 ohos/ 内 ArkTS 代码变更需要重新构建 HAP 并重新安装。
9.12 flutter build hap 卡在 ohpm install:BATCH RECURSION exceeds STACK limits
Windows 环境下最坑的一个问题,报错如下:
> hvigor start to execute ohpm install
****** B A T C H R E C U R S I O N exceeds STACK limits ******
Recursion Count=242, Stack Usage=90 percent
> hvigor ERROR: 00306053 Specification Limit Violation
Error Message: ohpm install failed.
排查路径:
- 手动
ohpm install完全正常 → 排除网络与依赖问题; bash里直接hvigorw assembleHap正常 → 说明与 flutter 的调用链有关;- 读
hvigorw.js发现它无条件把process.env.ohpmBin覆盖成 DevEco 自带的ohpm.bat;ohpm.bat的首参只要包含ohpm就会进逐字符递归; - 对比
PATH:Git Bash 的usr/bin里有一个无扩展名的ohpmshell 脚本,命令解析走了脚本分支,参数被整体当成%1,递归层数正好等于整条命令行长度。
解决:构建时使用不含 Git usr/bin 的 PATH(保留 Git/cmd 供 flutter 找 git),或改用 8.4 的 hvigorw assembleHap 命令。
9.13 spawn java ENOENT
越过 ohpm 之后,打包阶段(PackageHap)报:
> hvigor ERROR: Failed :entry:default@PackageHap...
> hvigor ERROR: Error Code: 00308018 Unknown Error
spawn java ENOENT
PackageHap 用 java 跑打包工具,环境里没有 java 就报这个「Unknown Error」。补 DevEco 自带 JBR(<DevEco Studio>/jbr)或设置 JAVA_HOME 即可,与工程代码无关。
相关链接
更多推荐


所有评论(0)