开发工具: 华为云码道

本文配套仓库: 上游 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 真机运行。

插件目前支持 androidiosohos 平台,源码位于 GitHub 上游仓库。文中的代码以提交 50d4a32ce524d8235a20527e6a5b69e136f6fef0 为参考。

在这里插入图片描述

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

在这里插入图片描述


应用主界面 授权弹窗 授权后返回真实 OAID

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 SDK3.44.9+ohos-0.0.1-canary1Flutter 编译与 OHOS 平台工具链
Flutter 分支1.0.3-ohos-1.0.0-beta.1本机 Flutter 工具链所在分支
Dart SDK3.12.2Dart 语言与包管理环境
HarmonyOS 开发套件26.0.0(API 26)开发套件版本及对应的 API 级别
compatibleSdkVersion5.1.0(18)当前工程声明的最低兼容版本
插件版本1.0.3pubspec.yaml 中的包版本
原生语言ArkTSHarmonyOS 插件实现
插件产物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"
}

在这里插入图片描述

本工程未显式声明 compileSdkVersiontargetSdkVersion,DevEco Studio 按开发套件默认值(API 26)编译和设定行为版本,最低兼容 API 18。安装后能否取得设备标识,还取决于设备系统是否提供 OAID 能力以及用户的授权情况。


三、从源码仓库开始准备适配工程

3.1 将上游源码同步到 AtomGit

适配已有第三方插件时,先从 Pub 包信息或项目 README 确认上游地址、包名、版本和许可证。同步仓库时保留源码、许可证和提交历史,方便后续跟踪上游更新。

在 AtomGit 网页的新建仓库流程中,找到从已有仓库导入的入口,填写上游 Git 地址,选择自己有写权限的组织或个人空间,设置目标仓库名称后执行导入。完成后检查目标分支、pubspec.yamlLICENSE 和提交历史是否完整,并记录适配起点的提交号。如果上游本身托管在 AtomGit,也可以通过 Fork 获得自己的工作仓库。

device_platform_uid 的适配代码直接位于 GitHub 上游仓库 deepak07082/device_idmain 分支。如果需要同步到 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.yamllib/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.yamllib/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.dartPigeon 生成的 BasicMessageChannel 通道代码
pigeons/platform_device_id.dartPigeon 接口契约,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.dartlib/device_id_api.dartpigeons/platform_device_id.dart,再在 ohos/src/main/ets/components/plugin/DeviceIdPlugin.ets 中实现对应的原生调用。适配其他已有平台的插件时,也应保留其公开接口和其他平台实现。

本例的对应关系如下:

Dart 入口或模型通道协议OHOS 实现应保持的行为
DeviceId().getDeviceId()Pigeon @HostApiString 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 侧之间只有一条通道:

  1. BasicMessageChannel('dev.flutter.pigeon.device_id.DeviceIdApi.getDeviceId'):由 Pigeon 生成,负责发送 getDeviceId 请求并接收回包,编解码器为 StandardMessageCodec

Flutter 页面

DeviceId 对外 API

DeviceIdApi Pigeon 生成代码

BasicMessageChannel pigeon 通道

ArkTS DeviceIdPlugin

APP_TRACKING_CONSENT 运行时授权

identifier.getOAID

与手写 MethodChannel 的插件不同,Pigeon 插件的 Dart 通道代码是生成产物:请求发送、回包解析和异常分支都由 lib/device_id_api.dart 承担,原生侧要做的就是把同名 BasicMessageChannel 的消息处理和回包格式实现正确。

4.1.1 一次完整获取的时序
HarmonyOS OAID 服务 DeviceIdPlugin.ets DeviceIdApi(Pigeon 生成) Flutter App HarmonyOS OAID 服务 DeviceIdPlugin.ets DeviceIdApi(Pigeon 生成) Flutter App DeviceId().getDeviceId() BasicMessageChannel.send(null) checkAccessTokenSync 检查授权 requestPermissionsFromUser(未授权时) 授权结果 identifier.getOAID() OAID 字符串 reply([oaid]) Future<String> 完成

4.2 报文模型:lib/device_id_api.dart

原生回包是一个 StandardMessageCodec 列表,Pigeon 生成的 Dart 代码按长度和首元素解释它。以下节选自 lib/device_id_api.dartgetDeviceId()

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] 多元素列表PlatformExceptioncode 取首元素
[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 方法没有参数,请求体固定为 nullmessageChannelSuffix 为空串时通道名不带后缀。回包按 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_errorgetDeviceId_errorDeviceIdPlugin.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 实现 FlutterPluginAbilityAware。下面列出类中的主要成员和方法,完整文件还包含 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 生命周期;
  • AbilityAwareAbilityPluginBinding 用于获取 UIAbility,运行时授权弹窗需要它的 context;
  • BasicMessageChannel 接收 Dart 发来的 Pigeon 请求;
  • identifier 提供 OAID 系统能力;
  • abilityAccessCtrlbundleManager 用于检查并请求 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_ohosindex.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——凭记忆写成的 applicationInfoBundleInfo 上并不存在,以 @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.mdOHOS 新增能力、适配版本、兼容限制与测试范围
LICENSE保留上游许可证
example/README.md依赖方式、运行目录、签名、操作步骤与效果图;覆盖获取与错误处理
pubspec.yamlohos/oh-package.json5核对包名、版本、插件注册、仓库地址、许可证和依赖
.gitignore忽略构建缓存及本机签名材料,不漏提交必要源码和配置
docs/OHOS_Adaptation_Blog.md适配路线、代码对照、关键决策、真机证据与踩坑复盘

本例的包名为 device_platform_uid,版本为 1.0.3,采用 MIT 许可证;Flutter 和 HarmonyOS SDK 版本写入环境说明。仓库未使用 README.OpenSourceNOTICE 模板,来源与版本信息记录在 README.OpenHarmony_CN.mdpubspec.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_uidpath 配置替换为下面的 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.lockdevice_platform_uid 的来源为 git,并核对 urlrefresolved-ref。同时检查没有 dependency_overridespubspec_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,避免页面销毁后继续调用 setStategetDeviceId() 是一次性的请求-回包调用,没有需要主动取消的订阅或流;页面退出时无需额外清理。

多个页面都需要设备 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.dartMockDeviceIdApi 验证 getDeviceId() 的返回值;example/test/widget_test.dartexample/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 模块配置自动签名:

  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。真机安装应选择与当前设备匹配的已签名产物。

8.5 在设备上测试设备 ID 获取

  1. 打开应用,页面自动发起首次获取;
  2. 首次获取时系统弹出 APP_TRACKING_CONSENT 授权对话框;
  3. 选择允许后,界面显示真实 OAID(非全零);
  4. 点击“重新获取”,确认多次获取返回同一标识,并记录耗时;
  5. 点击“复制”,将设备 ID 复制到剪贴板;
  6. 拒绝授权后再次获取,确认界面返回全零值 00000000-0000-0000-0000-000000000000,不崩溃。

页面初始的 -- 来自 Demo 默认值,首次获取完成后的显示才是系统返回结果。

8.6 鸿蒙设备运行效果

完成适配后,Flutter 应用调用同一个 getDeviceId() 即可在鸿蒙设备上取得 OAID 形式的设备唯一标识,并在授权前后观察到全零值与真实值的区别。

下面展示应用主界面、授权弹窗和授权后返回真实 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 的版本是否匹配。

处理顺序:

  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 在安装版本门槛上是满足的;但设备还必须提供 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.json5modules 是否包含 ./entry。同步失败时,Project Structure 无法正确解析模块,签名界面也可能不显示 entry。

9.3 无法手动签名

签名配置依附于可构建的应用模块和 product。只有 HAR 插件模块、工程 Sync 失败,或者打开了错误目录时,DevEco Studio 都可能无法提供 entry 签名入口。

建议先确认:

  • 打开的是 example/ohos
  • SDK 组件完整并且 Sync 成功;
  • entry 的模块类型为 entry
  • default product 和 target 已正确关联;
  • 当前账号、证书和调试设备状态有效。

9.4 能安装但获取不到设备 ID

按以下顺序检查:

  1. 首次调用是否弹出 APP_TRACKING_CONSENT 授权对话框;
  2. entry 是否声明 APP_TRACKING_CONSENTreasonusedScene 是否完整;
  3. 系统是否授予该权限;
  4. hilog 中是否出现 APP_TRACKING_CONSENT granted: true
  5. hilog 中是否出现 getOaid success
  6. 返回值是否为全零 00000000-0000-0000-0000-000000000000——这是权限未授予时的系统行为,授权后重新获取;
  7. 目标设备型号和系统是否真正提供 OAID 标识服务。

9.5 MissingPluginException

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

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

如果仍然出现,检查自动生成的插件注册文件中是否包含 DeviceIdPlugin,同时核对 pubspec.yamlohos/index.etsoh-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.lockresolved-ref。原生代码变动后停止应用并重新构建运行,不能只做 Dart 热重载。

9.11 获取到的设备 ID 是全零值

全零 00000000-0000-0000-0000-000000000000 是系统在 APP_TRACKING_CONSENT 未授予时的固定返回,不是适配缺陷。核对:

  1. 是否在系统弹窗中拒绝了授权——拒绝后插件按官方语义原样透传全零值;
  2. hilogAPP_TRACKING_CONSENT granted 的值是否为 true
  3. 授权后重新调用 getDeviceId() 是否返回真实标识(参见 8.5 的测试步骤)。

OAID 是系统级稳定标识,应用层无法重置;如需更换,请在系统设置中重置广告标识符。

相关链接

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐