手上有一个做了好几年的 Android 项目,业务逻辑、网络请求、本地缓存、第三方 SDK 全混在一起。现在要迁移到 HarmonyOS,第一反应是找一份 API 对照表——Activity 对应什么、SharedPreferences 对应什么。但真正开始迁移以后,会发现 API 对照反而不是最麻烦的部分。

最麻烦的问题是:原来的代码到底应该直接复用、封装后复用、替换实现,还是彻底重写?这个判断搞错了,后面要么写出一堆 if Android / if HarmonyOS 的兼容代码,要么为了硬复用把整个工程拖慢。

这篇不讲 API 怎么一一对应,而是从工程拆分的角度,聊聊一个中型 Android 项目迁到 HarmonyOS 时,哪些该留、哪些该拆、哪些必须重写。

一、迁移第一步不是改代码,而是先拆工程

很多人迁移的时候上来就把整个 Android 工程导入 DevEco Studio,然后开始一个文件一个文件改。这样做的结果是,改了一半发现整个工程结构不对,UI 框架不兼容、编译工具链不一样、依赖管理方式不同,越改越乱。

我在第一个迁移项目里就犯了这个错。后来第二次迁移的时候,我先花了一周时间做工程拆分,不急着写一行 ArkTS 代码。

拆分的思路是把 Android 工程按职责分层:UI 层、业务逻辑层、数据层、网络层、本地存储层、第三方 SDK 层。然后对每一层做一个判断:这一层在 HarmonyOS 上能不能直接用?不能直接用的话,是封装一层接口复用,还是必须重写?

这个判断的核心标准是:这一层依赖 Android 平台 API 有多深。UI 层几乎全是 Android 平台组件,必须重写;纯业务逻辑(比如订单计算、数据校验规则)不依赖平台,可以直接复用;数据层依赖 Android 的 SharedPreferences 和 SQLite,需要封装后迁移;网络层如果用了标准 HTTP 库,换一个 HarmonyOS 的网络实现就能复用业务逻辑;第三方 SDK 最麻烦,得逐个确认有没有 HarmonyOS 版本。

二、页面层通常是最不值得硬搬的一部分

UI 层是迁移量最大的部分,但也是最不值得硬搬的部分。

Android 的 Activity、Fragment、RecyclerView、各种自定义 View,和 HarmonyOS 的 ArkUI 声明式 UI 完全是两套思路。你不能把一个 Android 的 Activity 生命周期映射到 HarmonyOS 页面上,然后强行保持一样的结构——ArkUI 的状态管理和组件树渲染方式跟 Android 完全不同,硬搬只会写出一堆别扭的代码。

我现在的做法是:UI 层直接用 ArkUI 重写。但不是从零开始设计,而是参考原来的页面布局和交互逻辑,用 ArkTS 的方式重新实现。原来的 XML 布局文件不要直接翻译,而是拿过来当设计稿看——上面有什么控件、什么交互、什么数据绑定关系,理解了之后用 ArkUI 的方式重新写。

页面生命周期也要重新理解。Android 的 onCreate/onStart/onResume 那一套,在 HarmonyOS 里对应的是 aboutToAppear、onPageShow、onPageHide、aboutToDisappear。但不只是换个函数名那么简单——ArkUI 的状态驱动渲染模型,意味着你不需要像 Android 那样在生命周期里手动刷新 UI,状态变了 UI 自动更新。这个思维转变不过来,写出来的 ArkUI 代码会带着很重的 Android 味道。

三、数据层反而有不少东西可以留下

跟 UI 层相反,数据层是迁移过程中最能"捡现成"的部分。

业务模型(比如 User、Order、Product 这些实体类)通常不依赖任何平台 API,就是纯数据结构。这些类可以直接搬过来,稍微改一下语法就行,不用重写。Repository 层的业务逻辑也类似——它负责调用数据源、组合数据、处理业务规则,如果它内部不直接依赖 Android 的 SharedPreferences 或 SQLite API,那大部分逻辑都能复用。

需要改的是数据访问的实现。Android 的 SharedPreferences 在 HarmonyOS 里对应 Preferences;Android 的 SQLite 对应 RelationalStore。但这只是换了底层存储,上层 Repository 怎么组织数据、怎么缓存、怎么提供给 UI 层,逻辑是一样的。

这里我倾向于把存储访问封装成接口,业务层只依赖接口,不依赖具体实现。这样 Android 版和 HarmonyOS 版各自实现接口,业务逻辑不用改。下面是一个 StorageService 的接口定义和 HarmonyOS 实现示例,放在 ets/services/StorageService.ets 里。

export interface IStorageService {
  getString(key: string, defaultValue: string): string;
  putString(key: string, value: string): void;
  getNumber(key: string, defaultValue: number): number;
  putNumber(key: string, value: number): void;
  delete(key: string): void;
}

import preferences from '@ohos.data.preferences';

export class HarmonyStorageService implements IStorageService {
  private pref: preferences.Preferences | null = null;

  private async getPref(): Promise<preferences.Preferences> {
    if (!this.pref) {
      this.pref = await preferences.getPreferences(getContext(), 'app_storage');
    }
    return this.pref;
  }

  async getString(key: string, defaultValue: string): Promise<string> {
    const pref = await this.getPref();
    return (await pref.get(key, defaultValue)) as string;
  }

  async putString(key: string, value: string): Promise<void> {
    const pref = await this.getPref();
    await pref.put(key, value);
    await pref.flush();
  }

  async getNumber(key: string, defaultValue: number): Promise<number> {
    const pref = await this.getPref();
    return (await pref.get(key, defaultValue)) as number;
  }

  async putNumber(key: string, value: number): Promise<void> {
    const pref = await this.getPref();
    await pref.put(key, value);
    await pref.flush();
  }

  async delete(key: string): Promise<void> {
    const pref = await this.getPref();
    await pref.delete(key);
    await pref.flush();
  }
}

这段代码要解决的问题是:业务层不直接依赖 HarmonyOS 的 Preferences API,而是通过 IStorageService 接口访问存储。这样如果以后要换存储实现(比如换成 RelationalStore),或者在 Android 版里用 SharedPreferences 实现同一个接口,业务代码一行都不用改。

实际迁移时要注意:Preferences 的 API 是异步的,而 Android 的 SharedPreferences 是同步的。这意味着调用方需要改成 await 方式。不要为了"快速迁移"在接口层把异步包成同步——那样会引入不必要的复杂度,还容易出问题。另外,flush() 的时机要注意,频繁 flush 会影响性能,可以在页面退出或数据批量更新后统一 flush。

四、第三方 SDK 决定了不少实际迁移成本

第三方 SDK 是迁移成本最高的部分,没有之一。

UI 层重写虽然量大,但逻辑清晰,照着原来的页面做就行。数据层换个实现也不算太难。但第三方 SDK 不一样——推送、统计、支付、地图、IM,这些 SDK 如果没有 HarmonyOS 版本,你要么找替代方案,要么自己封装一个兼容层,要么这个功能就砍掉。

我一般会把第三方 SDK 分三类处理:

有官方 HarmonyOS 版本的,直接替换成 HarmonyOS SDK,API 调用方式可能略有不同,但功能能保住;

没有官方版本但有开源替代的,换一个功能类似的开源库。比如原来用某地图 SDK,没有 HarmonyOS 版,就看能不能用 Web 组件嵌入网页版地图,或者换一个支持 HarmonyOS 的地图服务;

完全没有替代方案的,要么砍掉这个功能,要么暂时用 Web 方案兜底,等官方 SDK 出来再换。

这里最忌讳的是为了兼容两个平台,在业务代码里写 if (isAndroid) { 调 Android SDK } else { 调 HarmonyOS SDK }。这样写短期能跑,但维护成本会越来越高。正确的做法是在 SDK 层封装一个统一接口,Android 版和 HarmonyOS 版各自实现这个接口,业务层只调接口。

五、平台差异最好统一收口

迁移过程中最大的风险不是某个功能做不出来,而是平台差异散落在整个工程里。

今天在这个页面里判断一下"如果是 HarmonyOS 就走这个逻辑",明天在那个工具类里加一个"Android 版本走另一条路",过几个月整个工程里到处都是平台判断,想改都不知道从哪下手。

我的做法是把所有平台相关的调用都收口到一个 PlatformService 里。业务层只调用 PlatformService 暴露的统一方法,不直接碰平台 API。这样平台差异只有一个地方需要维护,不会散落各处。

比如获取设备信息、打开系统设置、调用系统分享、检查网络状态——这些操作 Android 和 HarmonyOS 的实现不一样,但业务层不需要知道这些区别。PlatformService 内部根据当前平台调用对应的 API,对业务层暴露统一接口。

这样做的好处是:新增平台支持的时候,只需要给 PlatformService 加一个新实现,业务代码不用动。

六、哪些代码能复用,哪些我会直接重写

总结一下我的判断标准:

直接复用:纯数据模型、纯业务计算逻辑(不依赖平台 API 的那部分)、网络请求的业务参数组装和响应解析。

封装后复用:Repository 层、数据访问层、网络层的接口定义。通过接口隔离,具体实现按平台重写。

HarmonyOS 重写:所有 UI 页面、所有自定义 View、所有平台相关的系统调用。

替换实现:第三方 SDK。有 HarmonyOS 版的换官方 SDK,没有的找替代方案。

直接砍掉:迁移成本太高且业务价值不大的功能,先砍掉,后续按需加回来。

这里我更倾向于直接重写 UI,而不是为了复用硬套一层。因为 ArkUI 的声明式编程模型和 Android 的命令式 UI 差异太大,硬套一层抽象层反而会让代码更难维护。UI 层重写虽然工作量大,但写出来的代码是符合 ArkUI 范式的,后续维护也方便。

七、中型 Android 项目我会按这个顺序迁移

最后说说迁移顺序。我一般按这个节奏来:

先拆工程,把各层职责理清楚,确定哪些复用哪些重写;

搭好 HarmonyOS 工程骨架,把 PlatformService、StorageService 这些基础设施接口先建好;

迁移数据层和网络层,把 Repository 接口和实现搭起来,确保数据能正常存取;

迁移核心业务逻辑,纯逻辑的代码直接搬过来;

逐页重写 UI,从列表页、详情页这种核心页面开始,先跑通主流程;

处理第三方 SDK,逐个确认和替换;

最后做兼容性测试和性能优化。

不要想着一次把所有页面都迁移完。先把主流程跑通——用户能登录、能浏览、能完成核心操作——然后再逐步迁移次要页面。迁移过程中保留 Android 版本正常维护,不要等 HarmonyOS 版全部做完才切过去。

Android 迁移到 HarmonyOS,表面上是 API 替换,实际上是一次工程架构重新梳理的机会。原来的代码里可能堆了很多历史包袱,迁移的时候正好趁机分层、收口、去掉冗余代码。想清楚每层该怎么拆,比逐个查 API 对照表重要得多。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐