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 官方支持的完善,以后插件适配的流程肯定会越来越标准化、自动化。我们现在做这些实践,不仅能解决具体的技术需求,更能深入理解跨平台框架和原生系统是怎么"对话"的,为以后构建更健壮、兼容性更好的跨平台应用积累经验。

相关推荐
IT陈图图2 小时前
基于 Flutter × OpenHarmony 音乐播放器应用:构建录音文件列表区域
flutter·华为·鸿蒙·openharmony
不会写代码0002 小时前
Flutter 框架跨平台鸿蒙开发 - 实时彩票开奖查询应用开发教程
flutter·华为·harmonyos
夜雨声烦丿2 小时前
Flutter 框架跨平台鸿蒙开发 - 在线翻译拍照版应用开发教程
flutter·华为·harmonyos
小学生波波2 小时前
HarmonyOS6 - 鸿蒙读取系统联系人实战案例
arkts·鸿蒙开发·harmonyos6·读取联系人
小白阿龙2 小时前
鸿蒙+flutter 跨平台开发——物品过期追踪器开发实战
jvm·flutter·harmonyos·鸿蒙
猛扇赵四那边好嘴.2 小时前
Flutter 框架跨平台鸿蒙开发 - 亲子成长记录应用开发教程
flutter·华为·harmonyos
djarmy2 小时前
【开源鸿蒙跨平台 flutter选型】为开源鸿蒙跨平台工程集成网络请求能力,实现数据清单列表的完整构建与开源鸿蒙设备运行验证
flutter·华为·harmonyos
小白阿龙2 小时前
鸿蒙+flutter 跨平台开发——支持自定义的打印纸生成器实战
flutter·华为·harmonyos·鸿蒙
小风呼呼吹儿3 小时前
Flutter 框架跨平台鸿蒙开发 - 运动健身打卡:打造你的专属健身助手
flutter·华为·harmonyos