Flutter `flutter_udid` 库在鸿蒙(OpenHarmony)平台的适配实践

Flutter flutter_udid 库在鸿蒙(OpenHarmony)平台的适配实践

引言

鸿蒙生态的快速发展,让不少 Flutter 应用都开始考虑跨平台部署。flutter_udid 这个插件在 Android 和 iOS 上用得挺多,主要用来获取一个稳定的设备唯一标识(UDID)------它的最大特点是应用重装后标识不变,所以常用在用户追踪、数据关联这些业务场景里。

不过,官方并没有提供对鸿蒙(OpenHarmony)的原生支持。这就导致 Flutter 应用跑在鸿蒙设备上时,拿不到那个持久化的唯一标识,直接影响功能完整性。这篇文章会从 Flutter 插件的架构讲起,把它的通信机制拆解开,然后给出一套完整的适配方案,包括原理分析、代码实现、性能优化和最终的集成验证。目标不只是解决 flutter_udid 的问题,更希望能为其他 Flutter 插件适配鸿蒙提供一个可以参考的技术思路和实践模板。

一、先搞明白:技术背景和适配原理

1.1 Flutter 插件是怎么跟原生平台通信的?

Flutter 插件本质上就是 Dart 框架和原生平台(Android/iOS)之间的桥梁。它的核心是 平台通道(Platform Channel),两边通过约定好的消息编码格式(比如 StandardMessageCodec)进行异步通信。这个设计保证了 Flutter 本身是平台无关的。

  • MethodChannel :用于"方法调用-结果返回"这种模式。Dart 端调用一个方法,然后等着原生端返回结果(成功或失败)。这是插件最常用的通道,flutter_udid 的核心接口 getUDID() 就是基于它实现的。
  • 映射到鸿蒙 :我们要做的,就是在鸿蒙这一侧也建立一个对等的 MethodChannel 处理模块,让它能响应 Dart 端发过来的同名方法调用。

1.2 鸿蒙和 Android 有哪些关键区别?

要做适配,首先得弄清楚目标平台和原有平台到底哪里不一样。下面是一些关键点的对比:

特性维度 Android OpenHarmony (ArkTS) 适配影响与策略
开发语言 Java, Kotlin ArkTS (TypeScript 超集), C++ 需要用 ArkTS 重写原生代码,并适应它的异步编程模型(基于 Promise/Async)。
包管理与构建 Gradle, build.gradle Hvigor, build-profile.json5 需要创建鸿蒙模块(oh-package.json5),配置 HAP 构建依赖和 Native 能力。
设备标识 API Settings.Secure.ANDROID_ID TelephonyManager.getDeviceId() system.deviceInfo (需权限) deviceInfo.networkId deviceInfo.udid (受限) 不能直接照搬 Android API。得研究鸿蒙 @ohos.deviceInfo 接口怎么用、唯一性怎么样、要哪些权限。
持久化存储 SharedPreferences android.content.Context Preferences (@ohos.data.preferences) 用鸿蒙的 Preferences API 替代 SharedPreferences 来存、取 UDID。
项目结构 android/ 目录嵌入 Flutter 项目 独立的鸿蒙工程,通过 ohos/ 目录或子模块和 Flutter 项目关联 需要把 Flutter 项目改造成支持鸿蒙的混合工程结构。

1.3 flutter_udid 的核心工作流程

要有效适配,得先吃透原插件是怎么工作的。它的核心逻辑其实是一个 "生成-存储-读取" 的持久化流程:

  1. 首次获取 :应用第一次调用 getUDID() 时,原生端会尝试从安全存储(比如 SharedPreferences)里读取。如果读不到,就新生成一个随机的 UUID(版本4)。
  2. 持久化存储:把新生成的 UUID 以键值对的形式,写入平台提供的安全、持久的存储中。
  3. 后续读取:应用之后再调用,或者重启后再调用,都直接从那个持久化存储里读之前存好的 UUID 并返回。这样就保证了标识在应用生命周期内稳定不变。
  4. Dart 层缓存:为了提升性能,插件在 Dart 层会对获取到的 UDID 做内存缓存,避免频繁走平台通道调用。

适配关键点 :我们要做的,就是在鸿蒙端 "复现" 这一整套行为逻辑。只需要替换掉它底层的平台实现(比如标识生成的来源、存储用的 API),而保持 Dart 层的接口和行为完全不变。

二、动手之前:环境准备和工程改造

2.1 把开发环境搭起来

bash 复制代码
# 1. 确认 Flutter 环境 (推荐 3.13.0+,对鸿蒙支持更好)
flutter doctor -v
# 确保 Flutter SDK 包含了鸿蒙桌面设备支持

# 2. 配置鸿蒙原生开发环境
# - 下载安装 DevEco Studio 4.0 Release 或更高版本
# - 在 SDK Manager 里安装 HarmonyOS SDK (API 9+)
# - 安装对应的 ArkTS/JS SDK 和 Toolchains

# 3. 配置环境变量 (以 macOS/Linux 为例)
export HARMONYOS_SDK_HOME=~/Library/Huawei/Sdk
export PATH=$PATH:$HARMONYOS_SDK_HOME/toolchains:$HARMONYOS_SDK_HOME/openharmony
# 把 `ohos` 命令行工具加入 PATH

2.2 改造 Flutter 项目,让它支持鸿蒙

我们需要把普通的 Flutter 项目改造成支持鸿蒙的 "混合工程"

bash 复制代码
# 在你的 Flutter 项目根目录执行
flutter create --platforms=ohos .

# 执行后,项目会生成或更新一个 `ohos/` 目录,结构大概是这样:
# ohos/
# ├── AppScope/          # 应用级资源
# ├── entry/             # 主模块,Flutter 引擎的入口
# │   ├── src/
# │   │   └── main/
# │   │       ├── ets/
# │   │       │   ├── entryability/  # Ability 入口(类似 Android 的 Activity)
# │   │       │   └── pages/         # 页面
# │   │       └── resources/         # 模块资源
# │   ├── build-profile.json5        # 模块构建配置
# │   └── oh-package.json5           # 模块依赖声明
# └── build-profile.json5            # 工程级构建配置

这个命令会自动配置好 Flutter 引擎和鸿蒙 entry Ability 之间的桥接,是后面插件开发的基础。

三、核心步骤:用 ArkTS 实现鸿蒙原生代码

这部分是适配的核心。我们会在 entry 模块里创建插件的具体实现。

3.1 创建鸿蒙侧的 MethodChannel 处理器

文件路径ohos/entry/src/main/ets/FlutterUdidPlugin.ets

typescript 复制代码
// 导入鸿蒙系统能力接口
import deviceInfo from '@ohos.deviceInfo';
import preferences from '@ohos.data.preferences';
import { BusinessError } from '@ohos.base';
import { UIAbilityContext } from '@ohos.abilityKit';

// 定义和 Dart 端约定好的常量
const CHANNEL_NAME = 'com.example.flutter_udid/udid';
const STORE_KEY = 'flutter_udid';
const PREFERENCE_NAME = 'FlutterUdidStore';

export class FlutterUdidPlugin {
  private methodChannel: any; // 实际应该是 Flutter 引擎提供的 MethodChannel 类型,这里先用 any 表示
  private context: UIAbilityContext;
  private preferences: preferences.Preferences | null = null;

  // 构造函数,需要传入 Ability 的上下文
  constructor(context: UIAbilityContext) {
    this.context = context;
    // 注意:如何获取 Flutter 引擎的 MethodChannel 是个技术关键点。
    // 通常需要通过 Flutter 自动生成的 `FlutterOhosPlugin` 基类,或者特定的注册方式来获取。
    // 这里先假设我们能通过一个全局的插件注册表来设置。具体集成方式看下文。
  }

  // 初始化方法,Flutter 引擎在注册插件时会调用这个
  initialize(methodChannel: any): void {
    this.methodChannel = methodChannel;
    this.methodChannel.setMethodCallHandler(this.handleMethodCall.bind(this));
    this.initPreferences();
  }

  // 异步初始化持久化存储 Preferences
  private async initPreferences(): Promise<void> {
    try {
      // 获取应用沙箱路径下的 Preferences 文件
      this.preferences = await preferences.getPreferences(this.context, PREFERENCE_NAME);
      console.info('[FlutterUdidPlugin] Preferences 初始化成功。');
    } catch (error) {
      const err: BusinessError = error as BusinessError;
      console.error(`[FlutterUdidPlugin] 初始化 Preferences 失败: 错误码: ${err.code}, 信息: ${err.message}`);
      this.preferences = null;
    }
  }

  // 核心:处理从 Dart 端发过来的方法调用
  private async handleMethodCall(call: any): Promise<any> { // call.method, call.arguments
    switch (call.method) {
      case 'getUDID':
        return await this.getUdid();
      default:
        // 按照 Platform Channel 的规范,抛出未实现的异常
        return Promise.reject({
          code: 'notImplemented',
          message: `鸿蒙端暂未实现方法 ${call.method}。`,
          details: null
        });
    }
  }

  // 获取 UDID 的核心逻辑
  private async getUdid(): Promise<string> {
    // 1. 先尝试从持久化存储里读
    let storedUdid = await this.readUdidFromStore();
    if (storedUdid) {
      return storedUdid;
    }

    // 2. 存储里没有,就生成一个新的
    let newUdid = this.generateFallbackUdid();
    console.info(`[FlutterUdidPlugin] 生成了新的 UDID: ${newUdid}`);

    // 3. 把新生成的 UDID 存起来
    const success = await this.saveUdidToStore(newUdid);
    if (success) {
      return newUdid;
    } else {
      // 如果存失败了,还是把生成的 ID 返回,但要记录错误
      console.error('[FlutterUdidPlugin] 保存 UDID 到存储失败。');
      return newUdid; // 具体业务上也可以考虑抛出异常
    }
  }

  // 从 Preferences 里读取 UDID
  private async readUdidFromStore(): Promise<string | null> {
    if (!this.preferences) {
      await this.initPreferences();
      if (!this.preferences) return null;
    }
    try {
      const value = await this.preferences.get(STORE_KEY, '');
      return value !== '' ? value as string : null;
    } catch (error) {
      const err: BusinessError = error as BusinessError;
      console.error(`[FlutterUdidPlugin] 读取 UDID 失败: 错误码: ${err.code}, 信息: ${err.message}`);
      return null;
    }
  }

  // 把 UDID 保存到 Preferences
  private async saveUdidToStore(udid: string): Promise<boolean> {
    if (!this.preferences) {
      await this.initPreferences();
      if (!this.preferences) return false;
    }
    try {
      await this.preferences.put(STORE_KEY, udid);
      await this.preferences.flush(); // 确保数据写到磁盘
      return true;
    } catch (error) {
      const err: BusinessError = error as BusinessError;
      console.error(`[FlutterUdidPlugin] 保存 UDID 失败: 错误码: ${err.code}, 信息: ${err.message}`);
      return false;
    }
  }

  // 生成备用 UDID 的策略 (先尝试拿系统 ID,不行再生成随机 UUID)
  private generateFallbackUdid(): string {
    try {
      // 方案 A:尝试获取系统定义的 deviceId (需要权限 `ohos.permission.GET_SENSITIVE_PERMISSIONS`)
      // 注意:这个 ID 可能在清除应用数据或设备重置后变化,需要评估其唯一性和持久性。
      const networkId = deviceInfo.deviceId;
      if (networkId && networkId.length > 0) {
        // 可以对它做一次哈希处理,增加隐私安全性和格式统一性
        // 比如: return this.hashString(networkId);
        return `HARMONY_${networkId.substring(0, 8).toUpperCase()}`;
      }
    } catch (error) {
      console.warn('[FlutterUdidPlugin] 无法获取 deviceId,回退到随机 UUID。');
    }

    // 方案 B:生成随机 UUID (版本4)
    // 这是纯软件实现,不依赖硬件,兼容性最好,但完全依赖本地存储的持久性。
    return this.generateV4Uuid();
  }

  // 生成一个版本4的 UUID
  private generateV4Uuid(): string {
    // 简化示例,生产环境建议用更健壮的 UUID 生成算法
    const hexDigits = '0123456789abcdef';
    let uuid = '';
    for (let i = 0; i < 32; i++) {
      if (i === 12) {
        uuid += '4'; // version 4
      } else if (i === 16) {
        uuid += hexDigits[(Math.random() * 4) | 8]; // variant 10xx
      } else {
        uuid += hexDigits[(Math.random() * 16) | 0];
      }
      // 加连字符
      if (i === 7 || i === 11 || i === 15 || i === 19) {
        uuid += '-';
      }
    }
    return uuid;
  }
}

3.2 把插件注册到 Flutter 引擎

文件路径ohos/entry/src/main/ets/entryability/EntryAbility.ets

鸿蒙的 EntryAbility 是应用的入口点。我们需要在这里初始化和注册我们的插件。

typescript 复制代码
import { UIAbility } from '@ohos.abilityKit';
import window from '@ohos.window';
import { FlutterUdidPlugin } from '../FlutterUdidPlugin'; // 引入我们写的插件类
// 注意:下面的导入路径和类名取决于 Flutter 鸿蒙引擎模板的具体生成内容
import { FlutterOhosPluginRegistrant } from '@ohos/flutter_engine/FlutterOhosPluginRegistrant';

export default class EntryAbility extends UIAbility {
  private flutterUdidPlugin: FlutterUdidPlugin | null = null;

  onCreate(want, launchParam): void {
    console.info('[EntryAbility] onCreate');
    // 1. 初始化插件实例
    this.flutterUdidPlugin = new FlutterUdidPlugin(this.context);

    // 2. 关键步骤:把插件注册到 Flutter 引擎。
    // 这通常是通过一个全局的插件注册函数完成的。具体 API 可能随着 Flutter 对鸿蒙支持的迭代而变化。
    // 假设存在下面这样的注册机制:
    FlutterOhosPluginRegistrant.registerCustomPlugin((engine) => {
      // 创建一个和 Dart 端同名的 MethodChannel
      const channel = new engine.MethodChannel(engine.dartExecutor, 'com.example.flutter_udid/udid');
      // 把 channel 传给我们的插件进行初始化
      this.flutterUdidPlugin!.initialize(channel);
      return this.flutterUdidPlugin;
    });
  }

  onWindowStageCreate(windowStage: window.WindowStage): void {
    console.info('[EntryAbility] onWindowStageCreate');
    windowStage.loadContent('pages/Index', (err) => {
      if (err.code) {
        console.error(`[EntryAbility] 加载页面内容失败。错误码: ${err.code}, 信息: ${err.message}`);
        return;
      }
      console.info('[EntryAbility] 页面内容加载成功。');
    });
  }

  onDestroy(): void {
    console.info('[EntryAbility] onDestroy');
    this.flutterUdidPlugin = null;
  }
}

重要说明 :上面这种注册方式 (FlutterOhosPluginRegistrant) 目前还只是概念示意。Flutter 对鸿蒙的插件注册机制可能还在完善中,实际实现时需要参考最新的 Flutter 引擎源码,或者遵循特定的插件开发模板。

3.3 配置模块依赖和权限

文件路径ohos/entry/oh-package.json5

json5 复制代码
{
  "license": "MIT",
  "devDependencies": { },
  "name": "@ohos/entry",
  "description": "Please describe the basic information.",
  "main": "ets/entryability/EntryAbility.ets",
  "version": "1.0.0",
  "dependencies": {
    // 确保依赖了 Flutter 引擎模块
    "@ohos/flutter_engine": "file:../../flutter/build/ohos/engine",
    "@ohos/hmos_sdk": "^1.0.0" // 或对应的 SDK 版本
  }
}

文件路径ohos/entry/src/main/module.json5

需要在 module.json5 里声明插件要用到的系统权限。

json5 复制代码
{
  "module": {
    "requestPermissions": [
      {
        "name": "ohos.permission.GET_SENSITIVE_PERMISSIONS", // 如果需要获取 deviceId 就得申请这个
        "reason": "用于生成更稳定的设备唯一标识",
        "usedScene": {
          "ability": ["EntryAbility"],
          "when": "always"
        }
      }
    ]
  }
}

四、Dart 侧代码的调整和封装(建议做)

虽然 flutter_udid 的 Dart 接口是标准的,但为了更好的兼容性和错误处理,可以专门为鸿蒙做一个包装层,或者对原插件进行条件导入。

4.1 创建一个平台兼容层

dart 复制代码
// lib/harmony_udid.dart
import 'dart:async';
import 'package:flutter/services.dart';

class HarmonyUdid {
  static const MethodChannel _channel =
      const MethodChannel('com.example.flutter_udid/udid');

  static Future<String?> get udid async {
    try {
      final String? result = await _channel.invokeMethod('getUDID');
      return result;
    } on PlatformException catch (e) {
      print("从鸿蒙端获取 UDID 失败: '${e.message}'。");
      // 返回一个临时的本地生成 ID(不持久)
      return _generateLocalFallbackId();
    } catch (e) {
      print("获取 UDID 时发生意外错误: $e");
      return null;
    }
  }

  static String _generateLocalFallbackId() {
    // 一个简单的本地回退方案,只用于当前会话
    return 'harmony_fallback_${DateTime.now().millisecondsSinceEpoch}';
  }
}

4.2 在应用里使用

dart 复制代码
import 'package:your_app/harmony_udid.dart'; // 如果原插件已经通过条件编译支持鸿蒙了,也可以直接用原插件

void fetchDeviceId() async {
  String? deviceUdid = await HarmonyUdid.udid;
  if (deviceUdid != null) {
    print('在鸿蒙上获取到的设备 UDID: $deviceUdid');
    // 用到你的业务逻辑里...
  } else {
    print('没能获取到稳定的 UDID。');
  }
}

五、一些优化和安全方面的考虑

  1. 存储性能Preferencesflush() 操作是耗时的 I/O 操作。确保只在必要的时候调用(比如首次生成并保存时)。
  2. ID 生成策略
    • 隐私 :避免直接上传原始的、可能全局唯一的设备 ID(比如 deviceId)。建议对它做不可逆的哈希处理(比如 SHA-256),并且混入应用特有的盐值(Salt)。
    • 稳定性 :测试一下不同鸿蒙版本和机型上 deviceInfo.deviceId 的稳定性和获取成功率。明确什么情况下该用兜底的随机 UUID 方案。
  3. 错误恢复 :就像上面代码写的,当 Preferences 初始化失败或者读写异常时,要有清晰的日志和合理的回退机制(比如返回一个会话 ID),保证应用功能不会因此崩溃。
  4. 多进程情况 :如果你的 Flutter 应用在鸿蒙上涉及多进程,需要考虑 Preferences 的进程间同步问题,可能得用到分布式数据管理。

六、集成测试与验证

  1. 编译运行 :在 DevEco Studio 里打开 ohos 目录下的工程,连接鸿蒙真机或模拟器,运行 entry 模块。
  2. 功能测试
    • 首次安装应用,调用 getUDID,记下生成的 ID。
    • 杀掉应用再重新启动,再次调用 getUDID,看看 ID 是不是一样。
    • 关键测试 :清除应用数据(或者在鸿蒙设置里卸载重装),验证 ID 是否变化(按照设计,这时候应该会变,这正是"应用重装不变性"要体现的)。
  3. 看日志 :通过 hdc shell hilog 命令查看插件打印的日志,确认生成、存储、读取的流程没问题。
  4. 性能测试 :用鸿蒙的性能分析工具,测一下多次调用 getUDID 的耗时,确保 Dart 层的缓存生效了,没有频繁地进行平台通道交互。

七、总结

这篇文章详细介绍了把 flutter_udid 插件适配到鸿蒙平台的整个过程,从原理分析、环境配置、ArkTS 原生代码实现、Dart 层封装,到性能安全和测试验证,算是走通了一个全链路。适配的核心在于理解 Flutter 插件架构的抽象层,然后在新的原生平台上实现它约定好的接口和行为。

这套方法其实可以推广到大多数基于 MethodChannel 的 Flutter 插件的鸿蒙适配工作上。随着 OpenHarmony 生态的壮大和 Flutter 官方支持的完善,以后插件适配的流程肯定会越来越标准化、自动化。我们现在做这些实践,不仅能解决具体的技术需求,更能深入理解跨平台框架和原生系统是怎么"对话"的,为以后构建更健壮、兼容性更好的跨平台应用积累经验。

相关推荐
看谷秀2 天前
鸿蒙-part3-arkts下
arkts
TrisighT2 天前
ArkTS 的 @BuilderParam 你八成只用了皮毛——那个尾随闭包写法差点被我当 bug 删了
harmonyos·arkts·arkui
恋猫de小郭2 天前
Amper 正式转正 Kotlin Toolchain ,Gradle 未来何去何从
android·前端·flutter
张风捷特烈2 天前
Flutter 类库大揭秘#02 | path_provider 各平台实现
前端·flutter
TT_Close3 天前
别劝退了!5秒搞定 Flutter 鸿蒙 FVM 起跑线
flutter·harmonyos·visual studio code
TrisighT3 天前
ArkTS 列表滚动时为什么会闪现旧数据?我扒了 LazyForEach 的复用逻辑
harmonyos·arkts·arkui
你听得到113 天前
用户说 App 卡,但说不清在哪?我把 Flutter 监控 SDK 升级成了链路观测工作台
前端·flutter·性能优化
TrisighT4 天前
一个下午搞定 ArkTS 折叠面板?结果我从两点写到晚上九点
harmonyos·arkts·arkui
stringwu5 天前
Flutter 开发必备:MVI 架构的高效实现指南
前端·flutter
程序员老刘5 天前
Flutter版本选择指南:3.44系列继续观望 | 2026年6月
flutter·ai编程·客户端