Flutter 鸿蒙适配实战:oh_device_id 库适配HarmonyOS踩坑,获取设备唯一标识,解决多实例 ID 冲突、权限校验与打包兼容问题
开发工具: 华为云码道
本文配套仓库: 上游 deepak07082/device_id;鸿蒙适配改动位于该仓库的
main分支。
鸿蒙适配后仓库: https://atomgit.com/oh-flutter/device_id
device_platform_uid(仓库名 device_id)将设备唯一标识获取能力封装为 Flutter 插件,应用调用 getDeviceId() 即可在 Android 上取得 ANDROID_ID、在 iOS 上取得 identifierForVendor。本文以 device_platform_uid 1.0.3 为例,介绍源码准备、OHOS 工程配置、ArkTS 实现和 example 真机运行。
插件目前支持 android、ios、ohos 平台,源码位于 GitHub 上游仓库。文中的代码以提交 50d4a32ce524d8235a20527e6a5b69e136f6fef0 为参考。

OHOS 适配仓库地址https://atomgit.com/oh-flutter/device_id改动当前在 main 分支工作区,发布 TAG 为 1.0.3-ohos-1.0.0-beta.1。

| Example 启动 | 复制后内容 | 获取记录列表 |
|---|---|---|
| Example 启动进入页面 | 复制后显示Device_id内容 | 复制后显示获取次数的记录列表 |
以下是操作的视屏,可以参考一下:
一、插件简介与适配目标
设备唯一标识是移动应用的基础能力之一。device_platform_uid 在 Android 上读取 Settings.Secure.ANDROID_ID,在 iOS 上读取 identifierForVendor,业务层调用一个 getDeviceId() 就能拿到跨平台语义一致的匿名设备 ID,不需要自己适配各系统的标识接口。
例如,统计分析可以用它生成匿名用户标识,广告归因可以把它作为设备维度的 key,游戏存档也可以用它绑定本地进度。
适配 OpenHarmony / HarmonyOS 时,与之语义最接近的系统标识是 OAID(开放匿名设备标识符)。适配完成后,同样的 getDeviceId() 调用会返回设备 OAID:权限未授予时系统返回全零值,授权后返回真实标识。
二、环境准备
环境搭建参考社区文档: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 分支 | 1.0.3-ohos-1.0.0-beta.1 | 本机 Flutter 工具链所在分支 |
| Dart SDK | 3.12.2 | Dart 语言与包管理环境 |
| HarmonyOS 开发套件 | 26.0.0(API 26) | 开发套件版本及对应的 API 级别 |
compatibleSdkVersion | 5.1.0(18) | 当前工程声明的最低兼容版本 |
| 插件版本 | 1.0.3 | pubspec.yaml 中的包版本 |
| 原生语言 | ArkTS | HarmonyOS 插件实现 |
| 插件产物 | HAR | 被应用 entry 模块依赖 |
2.1 开发套件版本与工程中的 SDK 版本配置
26.0.0(API 26) 和 5.1.0(18) 涉及 HarmonyOS 开发套件版本(API 版本)及其底座 OpenHarmony 的版本号体系。在本文工程中,相关版本的含义与配置方式如下:
26.0.0(API 26)表示 HarmonyOS 开发套件版本为26.0.0,对应 API 26。5.1.0(18)是本文工程中compatibleSdkVersion的属性值,声明最低兼容 API 18。
对应的 product 配置为:
{
"name": "default",
"signingConfig": "default",
"compatibleSdkVersion": "5.1.0(18)",
"runtimeOS": "HarmonyOS"
}

本工程未显式声明 compileSdkVersion 和 targetSdkVersion,DevEco Studio 按开发套件默认值(API 26)编译和设定行为版本,最低兼容 API 18。安装后能否取得设备标识,还取决于设备系统是否提供 OAID 能力以及用户的授权情况。
三、从源码仓库开始准备适配工程
3.1 将上游源码同步到 AtomGit
适配已有第三方插件时,先从 Pub 包信息或项目 README 确认上游地址、包名、版本和许可证。同步仓库时保留源码、许可证和提交历史,方便后续跟踪上游更新。
在 AtomGit 网页的新建仓库流程中,找到从已有仓库导入的入口,填写上游 Git 地址,选择自己有写权限的组织或个人空间,设置目标仓库名称后执行导入。完成后检查目标分支、pubspec.yaml、LICENSE 和提交历史是否完整,并记录适配起点的提交号。如果上游本身托管在 AtomGit,也可以通过 Fork 获得自己的工作仓库。
device_platform_uid 的适配代码直接位于 GitHub 上游仓库 deepak07082/device_id 的 main 分支。如果需要同步到 AtomGit 或其他托管平台,按上面的导入/Fork 流程操作即可;本文直接使用 GitHub 地址拉取代码,需要提交修改时,使用自己有写权限的仓库或 Fork。
3.2 将代码拉取到宿主机
在安装了 Flutter OH 和 DevEco Studio 的开发电脑上打开终端,进入准备存放项目的目录,执行:
git clone https://github.com/deepak07082/device_id.git
cd device_id
pwd
ls
git remote -v
git status --short --branch
git rev-parse HEAD

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

图 1:在宿主机终端输入仓库拉取命令。
3.3 在仓库根目录确认分支与发布 TAG
接着在 device_id/ 根目录确认分支。本例的适配改动直接基于 main 分支提交(bae9fe0),并按 原库版本-ohos-版本号-beta.x 格式创建 TAG:
git branch --show-current
git tag 1.0.3-ohos-1.0.0-beta.1 -m "device_platform_uid 1.0.3 ohos adaptation (OpenHarmony/HarmonyOS)"
git tag -l
仓库中当前 TAG 1.0.3-ohos-1.0.0-beta.1 指向适配提交之前的 50d4a32,适配提交 bae9fe0 尚未推送。如需让 TAG 覆盖适配代码,将 TAG 重新指向适配提交后一并推送:
git tag -f 1.0.3-ohos-1.0.0-beta.1 bae9fe0c72c54239c4fa6bfa2464108fcede2b70
git push origin main
git push origin 1.0.3-ohos-1.0.0-beta.1
对已推送的 TAG 重建时,需要通知已经依赖旧 TAG 的使用方。

图 2:在 device_id 仓库根目录确认分支与发布 TAG。
3.4 自动补全 OHOS 适配结构
本例的适配在仓库已有 ohos/ 和 example/ohos/ 的基础上完成,直接运行示例时可以跳过结构补全。这里仍记录补全命令,供适配其他尚无 OHOS 结构的插件时参考。以下命令适用于尚无 ohos/ 目录的既有 Flutter 平台插件:
flutter create --template=plugin --platforms=ohos --project-name device_platform_uid .
git status --short
git diff -- pubspec.yaml lib example
--template=plugin指定插件模板。--platforms=ohos指定需要补全的平台。--project-name device_platform_uid使用 Dart 包名,避免把与包名不一致的仓库目录名device_id作为包名。- 最后的
.表示在当前插件目录补全工程,不是另建一层device_platform_uid/。
该命令生成 OHOS 平台脚手架,业务逻辑需要在 ArkTS 中实现。生成后通过 diff 检查 pubspec.yaml、lib/ 和 example/ 的变化,保留已有 API、其他平台注册项及依赖配置。不同 Flutter OH 版本生成的模板可能略有差异。旧版 Xcode(如 13.2.1)下 flutter create 内省 iOS 工程可能崩溃,追加 --org 参数传入组织名可以跳过这一内省步骤。
如果生成后 example/ohos/ 仍不存在,进入已有示例应用补全平台:
cd example
flutter create --platforms=ohos .
cd ..
新建插件则使用 flutter create --org com.example --template=plugin --platforms=ohos device_platform_uid;已有插件使用上面的 . 在当前目录补全。

图 3:在插件根目录输入 OHOS 结构补全命令。
3.5 适配后的项目目录
适配后的关键目录如下:
device_id/
├── lib/
│ ├── device_id.dart # 对外 API(DeviceId 类)
│ └── device_id_api.dart # Pigeon 生成的通道代码
├── pigeons/
│ └── platform_device_id.dart # Pigeon 接口契约
├── ohos/
│ ├── index.ets
│ ├── oh-package.json5
│ └── src/main/
│ ├── ets/components/plugin/DeviceIdPlugin.ets
│ └── module.json5
├── android/ # Android 平台实现
├── ios/ # iOS 平台实现
├── example/
│ ├── lib/main.dart
│ ├── ohos/entry/
│ └── integration_test/
├── test/
├── docs/ # 适配博客与真机截图
└── pubspec.yaml
项目根目录如下,其中包含 ohos/、example/ohos/,以及 OpenHarmony 中英文说明、变更记录和适配文档:

图 4:适配后的 device_id 项目根目录。
| 文件 | 主要职责 |
|---|---|
lib/device_id.dart | 为业务应用提供最简入口 |
lib/device_id_api.dart | Pigeon 生成的 BasicMessageChannel 通道代码 |
pigeons/platform_device_id.dart | Pigeon 接口契约,generate_pigeon.sh 的输入 |
DeviceIdPlugin.ets | 注册 Flutter 通道并调用 HarmonyOS 原生能力 |
插件 module.json5 | 声明 HAR 模块信息 |
示例 entry module.json5 | 声明宿主应用 Ability、设备类型和权限场景 |
example/lib/main.dart | 展示设备 ID 获取、耗时统计和获取记录 |
四、Dart 接口与通道分析
OHOS 实现需要遵循 Dart 层已有的方法、参数和返回值约定。先阅读 lib/device_id.dart、lib/device_id_api.dart 和 pigeons/platform_device_id.dart,再在 ohos/src/main/ets/components/plugin/DeviceIdPlugin.ets 中实现对应的原生调用。适配其他已有平台的插件时,也应保留其公开接口和其他平台实现。
本例的对应关系如下:
| Dart 入口或模型 | 通道协议 | OHOS 实现 | 应保持的行为 |
|---|---|---|---|
DeviceId().getDeviceId() | Pigeon @HostApi:String getDeviceId() | identifier.getOAID() | 返回 OAID 字符串,失败时按 Pigeon 错误格式回包 |
DeviceIdApi.getDeviceId() | dev.flutter.pigeon.device_id.DeviceIdApi.getDeviceId | 同名 BasicMessageChannel | 成功回 [result],失败回 [code, message, details] |
| Pigeon 回包格式 | StandardMessageCodec 编码的列表 | Reply<Object> | 回包结构不合规会导致 Dart 侧抛 channel-error 或类型错误 |
原生端需要保持通道名和回包结构一致。系统能力不可用时,按 Pigeon 错误格式回包,Dart 层以 PlatformException 抛出。
4.1 跨端架构与调用时序
Flutter 侧和 HarmonyOS 侧之间只有一条通道:
BasicMessageChannel('dev.flutter.pigeon.device_id.DeviceIdApi.getDeviceId'):由 Pigeon 生成,负责发送getDeviceId请求并接收回包,编解码器为StandardMessageCodec。
与手写 MethodChannel 的插件不同,Pigeon 插件的 Dart 通道代码是生成产物:请求发送、回包解析和异常分支都由 lib/device_id_api.dart 承担,原生侧要做的就是把同名 BasicMessageChannel 的消息处理和回包格式实现正确。
4.1.1 一次完整获取的时序
4.2 报文模型:lib/device_id_api.dart
原生回包是一个 StandardMessageCodec 列表,Pigeon 生成的 Dart 代码按长度和首元素解释它。以下节选自 lib/device_id_api.dart 的 getDeviceId():
if (pigeonVar_replyList == null) {
throw _createConnectionError(pigeonVar_channelName);
} else if (pigeonVar_replyList.length > 1) {
throw PlatformException(
code: pigeonVar_replyList[0]! as String,
message: pigeonVar_replyList[1] as String?,
details: pigeonVar_replyList[2],
);
} else if (pigeonVar_replyList[0] == null) {
throw PlatformException(
code: 'null-error',
message: 'Host platform returned null value for non-null return value.',
);
} else {
return (pigeonVar_replyList[0] as String?)!;
}
| 回包内容 | Dart 侧行为 |
|---|---|
null(没有收到回包) | 抛 PlatformException(code: channel-error),通道上没有注册原生处理方 |
[result] 单元素列表 | 正常返回,getDeviceId() 的 Future 以该字符串完成 |
[code, message, details] 多元素列表 | 抛 PlatformException,code 取首元素 |
[null] | 抛 PlatformException(code: null-error),Pigeon 契约声明返回非空 String |
getDeviceId() 在 Pigeon 契约中返回非空 String,因此原生侧必须始终回包:OAID 为空时回 ["unknown_device_id"],异常时回错误结构,具体见第五章。
4.3 公开 API 与平台接口
Pigeon 契约定义在 pigeons/platform_device_id.dart:
import 'package:pigeon/pigeon.dart';
()
abstract class DeviceIdApi {
String getDeviceId();
}
@HostApi() 表示从 Flutter 调向宿主平台的单向请求。修改契约后运行仓库根目录的 generate_pigeon.sh,即可重新生成 Dart、Kotlin 和 Swift 代码:
flutter pub run pigeon \
--input pigeons/platform_device_id.dart \
--dart_out lib/device_id_api.dart \
--kotlin_out android/src/main/kotlin/com/example/device_id/DeviceIdApi.kt \
--kotlin_package com.example.device_id \
--swift_out ios/Classes/DeviceIdApi.swift
业务只接触 lib/device_id.dart 中的 DeviceId 类,它将调用转发给 Pigeon 生成的 API:
import 'device_id_api.dart';
class DeviceId {
Future<String?> getDeviceId() async {
final api = DeviceIdApi();
final deviceId = await api.getDeviceId();
return deviceId;
}
}
对外返回 Future<String?>,实际取值为 OAID 字符串;PlatformException 会从 await 处抛出,业务侧需要捕获处理。
4.4 Dart 通道协议分析
4.4.1 通道名称必须两端完全一致
final String pigeonVar_channelName =
'dev.flutter.pigeon.device_id.DeviceIdApi.getDeviceId$pigeonVar_messageChannelSuffix';
final BasicMessageChannel<Object?> pigeonVar_channel =
BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
通道名称由 Pigeon 按 dev.flutter.pigeon.<包名>.<接口名>.<方法名> 规则生成。Dart 和 ArkTS 任何一端拼写不一致,send 都会因等不到应答而抛 channel-error。
4.4.2 请求发送实现
Future<String> getDeviceId() async {
final String pigeonVar_channelName =
'dev.flutter.pigeon.device_id.DeviceIdApi.getDeviceId$pigeonVar_messageChannelSuffix';
final BasicMessageChannel<Object?> pigeonVar_channel =
BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
final List<Object?>? pigeonVar_replyList =
await pigeonVar_sendFuture as List<Object?>?;
// 回包解析与异常分支见 4.2
}
Pigeon 的 @HostApi 方法没有参数,请求体固定为 null;messageChannelSuffix 为空串时通道名不带后缀。回包按 List<Object?>? 接收后进入 4.2 的分支解析。
4.4.3 异常分支与业务侧处理
业务侧捕获 PlatformException 并区分三类 code:
try {
final String? id = await DeviceId().getDeviceId();
} on PlatformException catch (e) {
// channel-error:通道两端不一致,或原生插件未注册
// null-error:原生回了 null,违反 Pigeon 非空契约
// getOAID_error / getDeviceId_error:原生回包的错误码
}
前两个由 Pigeon 生成代码抛出;getOAID_error 和 getDeviceId_error 是 DeviceIdPlugin.ets 定义的错误码,message 携带原生异常信息,details 固定为空串。排查时优先看 code,再对照原生 hilog 日志定位。
五、补全 OHOS 原生实现与工程配置
5.1 在 DeviceIdPlugin.ets 中实现原生能力
以 getDeviceId 为例:业务仍调用 DeviceId().getDeviceId(),Pigeon 通道仍发送同名请求。需要补全的是 DeviceIdPlugin.ets 中对 BasicMessageChannel 消息的处理:运行时请求 APP_TRACKING_CONSENT 授权,调用 identifier.getOAID() 读取设备标识,再按 Pigeon 封装格式回包。这样业务页面沿用原有接口即可取得 OHOS 的设备唯一标识。
DeviceIdPlugin 实现 FlutterPlugin 和 AbilityAware。下面列出类中的主要成员和方法,完整文件还包含 getUniqueClassName() 等插件接口。
原生插件位于:
ohos/src/main/ets/components/plugin/DeviceIdPlugin.ets
5.1.1 引入 Flutter 和 HarmonyOS 能力
import {
FlutterPlugin,
FlutterPluginBinding,
AbilityAware,
AbilityPluginBinding,
BasicMessageChannel,
StandardMessageCodec,
Reply,
} from '@ohos/flutter_ohos';
import identifier from '@ohos.identifier.oaid';
import abilityAccessCtrl, { Permissions } from '@ohos.abilityAccessCtrl';
import bundleManager from '@ohos.bundle.bundleManager';
import UIAbility from '@ohos.app.ability.UIAbility';
import common from '@ohos.app.ability.common';
import { BusinessError } from '@ohos.base';
import hilog from '@ohos.hilog';
其中:
FlutterPlugin负责接入 Flutter Engine 生命周期;AbilityAware与AbilityPluginBinding用于获取 UIAbility,运行时授权弹窗需要它的 context;BasicMessageChannel接收 Dart 发来的 Pigeon 请求;identifier提供 OAID 系统能力;abilityAccessCtrl和bundleManager用于检查并请求APP_TRACKING_CONSENT;BusinessError用于读取 HarmonyOS 异常码和异常消息;hilog用于原生侧诊断日志。
5.1.2 连接 Flutter Engine
private channel: BasicMessageChannel<Object> | null = null;
private ability: UIAbility | null = null;
onAttachedToEngine(binding: FlutterPluginBinding): void {
try {
this.channel = new BasicMessageChannel<Object>(
binding.getBinaryMessenger(),
DeviceIdPlugin.CHANNEL_NAME,
StandardMessageCodec.INSTANCE
);
this.channel.setMessageHandler({
onMessage: (message: Object, reply: Reply<Object>): void => {
this.onMessage(reply);
}
});
} catch (e) {
hilog.error(DeviceIdPlugin.DOMAIN, DeviceIdPlugin.TAG,
'onAttachedToEngine error: %{public}s', JSON.stringify(e));
}
}
Pigeon 使用 StandardMessageCodec,两端编解码器必须一致。setMessageHandler 传入内联对象字面量——@ohos/flutter_ohos 的 index.ets 未导出 MessageHandler 接口,直接 implements MessageHandler 会编译失败。Pigeon 请求不携带参数,onMessage 只需要 reply。
5.1.3 关联 UIAbility(AbilityAware)
onAttachedToAbility(binding: AbilityPluginBinding): void {
this.ability = binding.getAbility();
hilog.info(DeviceIdPlugin.DOMAIN, DeviceIdPlugin.TAG,
'onAttachedToAbility, ability attached: %{public}s',
this.ability != null ? 'yes' : 'no');
}
onDetachedFromAbility(): void {
this.ability = null;
}
运行时权限弹窗必须传入 UIAbility 的 context。实现 AbilityAware 后,引擎在 Ability 就绪时回调 onAttachedToAbility,插件保存引用备用;拿不到 context 时(如后台引擎场景)按 5.1.5 的兜底逻辑直接读取 OAID。
5.1.4 运行时请求 APP_TRACKING_CONSENT
private async requestOaidPermission(context: common.Context): Promise<boolean> {
const atManager = abilityAccessCtrl.createAtManager();
const bundleInfo = bundleManager.getBundleInfoForSelfSync(
bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION);
const tokenId = bundleInfo.appInfo.accessTokenId;
const grantStatus = atManager.checkAccessTokenSync(tokenId, DeviceIdPlugin.OAID_PERMISSION);
if (grantStatus === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) {
return true;
}
try {
const result = await atManager.requestPermissionsFromUser(
context, [DeviceIdPlugin.OAID_PERMISSION]);
return result.authResults.length > 0 &&
result.authResults[0] === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED;
} catch (e) {
hilog.warn(DeviceIdPlugin.DOMAIN, DeviceIdPlugin.TAG,
'requestPermissionsFromUser error: %{public}s', JSON.stringify(e));
return false;
}
}
先用 checkAccessTokenSync 检查,已授予则不再弹窗;未授予才调用 requestPermissionsFromUser。注意取 tokenId 的字段是 bundleInfo.appInfo.accessTokenId——凭记忆写成的 applicationInfo 在 BundleInfo 上并不存在,以 @ohos.bundle.bundleManager 的 .d.ts 为准。
5.1.5 处理消息并读取 OAID
private onMessage(reply: Reply<Object>): void {
try {
const ability = this.ability;
const context: common.Context | null =
ability != null ? ability.context : null;
if (context == null) {
this.readOaid(reply);
return;
}
this.requestOaidPermission(context).then((granted: boolean) => {
hilog.info(DeviceIdPlugin.DOMAIN, DeviceIdPlugin.TAG,
'APP_TRACKING_CONSENT granted: %{public}s', granted.toString());
this.readOaid(reply);
}).catch((e: BusinessError) => {
hilog.error(DeviceIdPlugin.DOMAIN, DeviceIdPlugin.TAG,
'requestOaidPermission error: %{public}s', JSON.stringify(e));
this.readOaid(reply);
});
} catch (e) {
hilog.error(DeviceIdPlugin.DOMAIN, DeviceIdPlugin.TAG,
'onMessage error: %{public}s', JSON.stringify(e));
reply.reply(["getDeviceId_error", "Failed to get device id", ""]);
}
}
private readOaid(reply: Reply<Object>): void {
try {
identifier.getOAID().then((oaid: string) => {
if (oaid == null || oaid.length == 0) {
reply.reply(["unknown_device_id"]);
} else {
reply.reply([oaid]);
}
}).catch((error: BusinessError) => {
hilog.error(DeviceIdPlugin.DOMAIN, DeviceIdPlugin.TAG,
'getOAID error: %{public}s', JSON.stringify(error));
reply.reply([
"getOAID_error",
error.message != null ? error.message : "Failed to get OAID",
""
]);
});
} catch (e) {
hilog.error(DeviceIdPlugin.DOMAIN, DeviceIdPlugin.TAG,
'readOaid error: %{public}s', JSON.stringify(e));
reply.reply(["getDeviceId_error", "Failed to get device id", ""]);
}
}
成功时回单元素列表 [oaid];OAID 为空时回 ["unknown_device_id"],与 Android 端取不到 ANDROID_ID 时的兜底语义一致;异常时回 [code, message, details] 三元素列表,Dart 侧转成 PlatformException。权限请求失败不阻断取值——系统会返回全零 OAID,插件按官方语义原样透传,如实暴露授权状态。
5.1.6 Engine 解绑时释放资源
onDetachedFromEngine(binding: FlutterPluginBinding): void {
try {
if (this.channel != null) {
this.channel.setMessageHandler(null);
this.channel = null;
}
} catch (e) {
hilog.error(DeviceIdPlugin.DOMAIN, DeviceIdPlugin.TAG,
'onDetachedFromEngine error: %{public}s', JSON.stringify(e));
}
}
Flutter Engine 销毁时清除消息处理器并释放通道引用,避免已销毁的引擎继续接收消息。
5.2 声明插件和宿主权限
获取 OAID 需要用户授权,本例在宿主应用中声明并运行时请求 ohos.permission.APP_TRACKING_CONSENT(user_grant 类型)。
5.2.1 插件 HAR 的权限
插件的 ohos/src/main/module.json5 是 HAR 模块声明,权限最终由安装的宿主应用生效,因此 HAR 中不声明 requestPermissions:
{
"module": {
"name": "device_platform_uid",
"type": "har",
"deviceTypes": ["default", "tablet"]
}
}
5.2.2 应用 entry 的权限
最终安装的是宿主应用。本例需要修改仓库根目录下的 example/ohos/entry/src/main/module.json5,在现有 module 配置中合并以下权限和使用场景,保留原有 Ability 等配置:
{
"module": {
"requestPermissions": [
{"name": "ohos.permission.INTERNET"},
{
"name": "ohos.permission.APP_TRACKING_CONSENT",
"reason": "$string:reason_oaid",
"usedScene": {
"abilities": ["EntryAbility"],
"when": "inuse"
}
}
]
}
}
权限原因资源合并到 example/ohos/entry/src/main/resources/base/element/string.json 的现有 string 数组中:
{
"string": [
{
"name": "reason_oaid",
"value": "用于获取设备匿名标识符(OAID)以提供设备唯一标识"
}
]
}
APP_TRACKING_CONSENT 是 user_grant 权限,reason(必须引用 string 资源)和 usedScene 缺一不可,否则构建报 00303218 Configuration Error。权限声明和运行时授权是两个步骤——本插件已通过 AbilityAware 在首次调用时自动请求(见 5.1.4),接入应用无需再写授权代码。
5.3 注册并导出插件
pubspec.yaml 通过以下配置声明 OHOS 插件类:
flutter:
plugin:
platforms:
ohos:
pluginClass: DeviceIdPlugin
插件的 ohos/index.ets 需要导出实现:
import DeviceIdPlugin from './src/main/ets/components/plugin/DeviceIdPlugin';
export default DeviceIdPlugin;
执行 flutter pub get 和构建后,Flutter 工具会为应用生成插件注册代码。通常不应手工编辑 GeneratedPluginRegistrant.ets,因为下次构建可能覆盖它。
注册异常的排查步骤见第九节 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.md | 原项目说明、OHOS 支持入口、配套 Demo 和文档链接;保留上游信息 |
README.OpenHarmony_CN.md | 简介、安装方式、版本对应关系、环境约束、权限、接口表、示例、已验证范围和遗留问题 |
README.OpenHarmony.md | 与中文说明对应的英文文档 |
CHANGELOG.OpenHarmony.md | OHOS 新增能力、适配版本、兼容限制与测试范围 |
LICENSE | 保留上游许可证 |
example/README.md | 依赖方式、运行目录、签名、操作步骤与效果图;覆盖获取与错误处理 |
pubspec.yaml、ohos/oh-package.json5 | 核对包名、版本、插件注册、仓库地址、许可证和依赖 |
.gitignore | 忽略构建缓存及本机签名材料,不漏提交必要源码和配置 |
docs/OHOS_Adaptation_Blog.md | 适配路线、代码对照、关键决策、真机证据与踩坑复盘 |
本例的包名为 device_platform_uid,版本为 1.0.3,采用 MIT 许可证;Flutter 和 HarmonyOS SDK 版本写入环境说明。仓库未使用 README.OpenSource 和 NOTICE 模板,来源与版本信息记录在 README.OpenHarmony_CN.md 和 pubspec.yaml 中。
6.2 提交前检查
提交前先完成第八节的插件、example 和真机测试,再从根目录检查改动:
git branch --show-current
git diff --check
git status --short
git diff --stat
git diff
检查 diff 中的接口、平台注册和依赖变化,移除本机路径及签名信息,并使文档中的版本和分支与提交内容一致。
6.3 提交并推送到 AtomGit
文档和代码整理完成后,在根目录暂存并提交。文件名按项目实际情况调整:
git add ohos pubspec.yaml example test docs .gitignore
git add README.md 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 platform support to device_platform_uid"
git remote -v
git branch --show-current
git push origin main
git push origin 1.0.3-ohos-1.0.0-beta.1
DevEco 可能向 example/ohos/build-profile.json5 写入本机签名配置,需要在提交前从暂存内容中移除。本例的适配提交为 bae9fe0c72c54239c4fa6bfa2464108fcede2b70,位于 main 分支。
推送后在托管平台发起合并请求,说明上游来源和版本、OHOS 实现范围、依赖及权限、测试环境、操作结果、已知限制,并附 Demo 运行图。目标分支和评审流程以接收仓库要求为准。
七、使用根目录 example 演示接入
仓库自带 example/,可以直接用来调试插件和体验设备 ID 获取。
7.1 本地适配时使用路径依赖
当前 example/pubspec.yaml 的依赖是:
dependencies:
flutter:
sdk: flutter
device_platform_uid:
path: ../
../ 相对于 example/pubspec.yaml 指向插件根目录,修改根目录插件后可直接联调。
7.2 通过 Git 引入插件
业务应用通过 Git 引入时,将 device_platform_uid 的 path 配置替换为下面的 Git 依赖。这里固定到本文使用的 TAG:
dependencies:
flutter:
sdk: flutter
device_platform_uid:
git:
url: https://github.com/deepak07082/device_id.git
ref: 1.0.3-ohos-1.0.0-beta.1
使用自己的适配版本时,先推送适配提交和 TAG,再将 url 改为对应仓库,ref 改为对应值。正式发布后可固定到 tag 或 commit。
从插件根目录执行:
cd example
flutter pub get
flutter pub deps
检查 example/pubspec.lock 中 device_platform_uid 的来源为 git,并核对 url、ref 和 resolved-ref。同时检查没有 dependency_overrides 或 pubspec_overrides.yaml 将其覆盖回本地依赖,确认应用使用的是 Git 依赖。
7.3 调用接口实现设备 ID 显示
下面的页面展示获取按钮和当前设备 ID,可用于 example/lib/main.dart。仓库中的完整 Demo 还提供获取耗时、获取次数和获取记录。
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:device_platform_uid/device_id.dart';
void main() {
runApp(const MaterialApp(home: DeviceIdPage()));
}
class DeviceIdPage extends StatefulWidget {
const DeviceIdPage({super.key});
State<DeviceIdPage> createState() => _DeviceIdPageState();
}
class _DeviceIdPageState extends State<DeviceIdPage> {
final DeviceId _deviceIdPlugin = DeviceId();
String _deviceId = '--';
bool _loading = false;
Future<void> _fetchDeviceId() async {
setState(() => _loading = true);
try {
final String? id = await _deviceIdPlugin.getDeviceId();
if (!mounted) return;
setState(() => _deviceId = (id == null || id.isEmpty) ? 'null' : id);
} on PlatformException catch (e) {
if (!mounted) return;
setState(() => _deviceId = 'PlatformException(${e.code})');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('获取失败:${e.message}')),
);
} finally {
if (mounted) setState(() => _loading = false);
}
}
void initState() {
super.initState();
_fetchDeviceId();
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Device ID')),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text('当前设备 ID:$_deviceId'),
const SizedBox(height: 16),
FilledButton(
onPressed: _loading ? null : _fetchDeviceId,
child: const Text('获取设备 ID'),
),
],
),
),
);
}
}
7.4 页面退出时的资源处理
异步回调先检查 mounted,避免页面销毁后继续调用 setState。getDeviceId() 是一次性的请求-回包调用,没有需要主动取消的订阅或流;页面退出时无需额外清理。
多个页面都需要设备 ID 时,可以在应用启动时获取一次并缓存,各页面共享同一份结果——OAID 是系统级稳定标识,同一设备多次获取结果一致。
八、验证、构建与鸿蒙设备运行效果
8.1 分别验证插件与 example
从插件仓库根目录执行:
flutter pub get
flutter analyze
flutter test
cd example
flutter pub get
flutter analyze
flutter test
接口测试应验证 Pigeon 回包解析、Mock API 注入和错误分支。仓库的 test/device_id_test.dart 用 MockDeviceIdApi 验证 getDeviceId() 的返回值;example/test/widget_test.dart 和 example/integration_test/plugin_integration_test.dart 覆盖页面与集成场景。Widget 测试应匹配实际保留的 Demo 页面;如果替换成第七节最小页面,也要相应调整原来的 UI 断言。
Dart 测试覆盖接口和页面逻辑,权限弹窗及 OAID 取值还需要在鸿蒙设备上验证。
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。真机安装应选择与当前设备匹配的已签名产物。
8.5 在设备上测试设备 ID 获取
- 打开应用,页面自动发起首次获取;
- 首次获取时系统弹出
APP_TRACKING_CONSENT授权对话框; - 选择允许后,界面显示真实 OAID(非全零);
- 点击“重新获取”,确认多次获取返回同一标识,并记录耗时;
- 点击“复制”,将设备 ID 复制到剪贴板;
- 拒绝授权后再次获取,确认界面返回全零值
00000000-0000-0000-0000-000000000000,不崩溃。
页面初始的 -- 来自 Demo 默认值,首次获取完成后的显示才是系统返回结果。
8.6 鸿蒙设备运行效果
完成适配后,Flutter 应用调用同一个 getDeviceId() 即可在鸿蒙设备上取得 OAID 形式的设备唯一标识,并在授权前后观察到全零值与真实值的区别。
下面展示应用主界面、授权弹窗和授权后返回真实 OAID 的页面效果:
| Example 启动 | 复制后内容 | 获取记录列表 |
|---|---|---|
| Example 启动进入页面 | 复制后显示Device_id内容 | 复制后显示获取次数的记录列表 |
以下是操作的视屏,可以参考一下:
OAID 依赖系统广告标识服务。即使系统版本满足要求,不同型号也可能存在能力差异,需要在目标设备上测试。
九、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 在安装版本门槛上是满足的;但设备还必须提供 OAID 标识服务,并满足签名和权限要求。
9.2 DevEco Studio 中看不到 entry 模块
插件的 ohos/ 目录是 HAR 模块,可安装应用的 entry 模块位于 example/ohos/entry。
请直接使用 DevEco Studio 打开:
device_id/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 能安装但获取不到设备 ID
按以下顺序检查:
- 首次调用是否弹出
APP_TRACKING_CONSENT授权对话框; - entry 是否声明
APP_TRACKING_CONSENT,reason和usedScene是否完整; - 系统是否授予该权限;
hilog中是否出现APP_TRACKING_CONSENT granted: true;hilog中是否出现getOaid success;- 返回值是否为全零
00000000-0000-0000-0000-000000000000——这是权限未授予时的系统行为,授权后重新获取; - 目标设备型号和系统是否真正提供 OAID 标识服务。
9.5 MissingPluginException
这通常表示 Dart 通道找不到已注册的原生插件。新增原生插件后需要重新构建应用。从仓库根目录执行:
cd example
flutter clean
flutter pub get
flutter run -d <device-id>
如果仍然出现,检查自动生成的插件注册文件中是否包含 DeviceIdPlugin,同时核对 pubspec.yaml、ohos/index.ets 和 oh-package.json5。
9.6 调用 getDeviceId() 抛 channel-error
channel-error 表示通道上没有收到任何回包。重点检查两处:
- Dart 侧通道名由 Pigeon 生成为
dev.flutter.pigeon.device_id.DeviceIdApi.getDeviceId; - ArkTS 侧必须注册同名的
BasicMessageChannel,并使用StandardMessageCodec编解码。
如果原生侧照搬插件模板的 MethodChannel 写法,或通道名拼写不一致,send 将永远等不到应答。回包结构同样重要:直接 reply.reply(oaid) 回字符串而不是 [oaid],Dart 侧的类型转换也会失败。
9.7 编译成功但安装失败
常见原因包括:
- HAP 未签名或使用了错误的 Profile;
- 设备未加入调试设备列表;
- 包名与签名 Profile 不匹配;
- 安装包的
compatibleSdkVersion高于设备 API; - 手机上已经安装了使用不同证书签名的同包名应用。
根据安装错误码区分签名、版本和包名冲突,再处理对应配置。
9.8 flutter create 不认识 ohos,或包名不合法
先执行 flutter --version,确认使用的是 OHOS 版工具链,环境配置回到第二节的社区链接核对。包名报错时,确认当前目录包含目标插件的 pubspec.yaml,并显式传入 --project-name device_platform_uid;仓库名 device_id 与 Dart 包名不一致,不能直接作为包名使用。旧版 Xcode 下 flutter create 内省 iOS 工程崩溃时,追加 --org 参数重试。生成后检查 diff,再补充 ArkTS 业务实现。
9.9 Git 依赖提示找不到 TAG 或无权限
先检查 URL 是否指向已同步的目标仓库,再确认 1.0.3-ohos-1.0.0-beta.1 已推送。TAG 尚未推送时,可以先使用第七节的提交号。私有仓库还需在本机配置 Git 认证。
9.10 改了本地 ArkTS,Demo 为什么没变化
先检查 example/pubspec.yaml:Git 依赖读取远程提交,不会自动读取本地插件改动。本地联调切回 path: ../;测试远程版本则先提交推送,再更新依赖并核对 pubspec.lock 的 resolved-ref。原生代码变动后停止应用并重新构建运行,不能只做 Dart 热重载。
9.11 获取到的设备 ID 是全零值
全零 00000000-0000-0000-0000-000000000000 是系统在 APP_TRACKING_CONSENT 未授予时的固定返回,不是适配缺陷。核对:
- 是否在系统弹窗中拒绝了授权——拒绝后插件按官方语义原样透传全零值;
hilog中APP_TRACKING_CONSENT granted的值是否为true;- 授权后重新调用
getDeviceId()是否返回真实标识(参见 8.5 的测试步骤)。
OAID 是系统级稳定标识,应用层无法重置;如需更换,请在系统设置中重置广告标识符。
相关链接
更多推荐




所有评论(0)