在这里插入图片描述

1. 写在前面:为什么想记录这段开发经历

第一次真正把一个 HarmonyOS 应用从零做到上架,前后大概花了两个多月。这两个月里,我踩过的坑比过去一年做 Android 开发时还多——不是鸿蒙本身有多难,而是它的开发范式、工程结构、调试手段和生态工具链,和过去熟悉的路径确实有不少差异。

写这篇文章的初衷很简单:把自己真实踩过的坑、验证过的方案、以及最后跑通的代码整理出来,让后来者少走一点弯路。文章不追求大而全,只讲我亲手写过、跑过、上线过的东西。

本文覆盖的内容包括:工程初始化与目录结构、ArkTS 与状态管理实践、页面路由与多设备适配、网络请求与数据持久化、常见踩坑记录、上架审核注意事项,以及最后的项目复盘。全文约 3000 字,关键位置都附了可运行的代码片段。

2. 工程初始化:先把地基打对

2.1 环境与版本

我使用的是 DevEco Studio 5.0 系列,配套 API 12(HarmonyOS NEXT)。这里第一个坑就来了:API 版本和 SDK 版本必须严格对齐,否则会出现编译通过但运行时报 Cannot find module 的情况。

建议在 build-profile.json5 里明确锁定编译版本:

{
  "app": {
    "products": [
      {
        "name": "default",
        "signingConfig": "default",
        "compatibleSdkVersion": "5.0.0(12)",
        "runtimeOS": "HarmonyOS"
      }
    ]
  }
}

compatibleSdkVersion 决定了应用能运行的最低系统版本,compileSdkVersion 决定编译时使用的 API。两者不要混用,否则很容易出现「本地能跑、真机崩溃」的问题。

2.2 目录结构约定

HarmonyOS 的工程结构是 Stage 模型,核心目录如下:

entry/
├── src/main/
│   ├── ets/
│   │   ├── entryability/      # UIAbility 入口
│   │   ├── pages/             # 页面
│   │   ├── components/        # 自定义组件
│   │   ├── viewmodel/         # 状态与业务逻辑
│   │   ├── model/             # 数据模型
│   │   └── utils/             # 工具函数
│   ├── resources/             # 资源文件
│   └── module.json5           # 模块配置

我的经验是:尽早把 components、viewmodel、model 三层拆开。鸿蒙的 ArkUI 声明式写法很容易让人把所有逻辑都堆在页面里,前期图快,后期改一个字段要翻五个文件。

3. ArkTS 与状态管理:最容易翻车的地方

3.1 @State、@Prop、@Link 的边界

ArkTS 的状态装饰器是新手最容易混淆的部分。简单总结我踩坑后的理解:

  • @State:组件内部状态,自己维护、自己修改。
  • @Prop:父组件传给子组件的单向副本,子组件改了不影响父组件。
  • @Link:父子双向同步,子组件修改会回传父组件。
  • @Observed + @ObjectLink:用于对象类型的深层观察。

一个典型错误是:把对象数组用 @State 声明,然后直接修改数组内元素的属性,发现 UI 不刷新。原因是 @State 只观察第一层引用变化。

@Observed
class TaskItem {
  id: number = 0;
  title: string = '';
  done: boolean = false;
}

@Component
struct TaskRow {
  @ObjectLink task: TaskItem;

  build() {
    Row() {
      Text(this.task.title)
        .decoration({ type: this.task.done ? TextDecorationType.LineThrough : TextDecorationType.None })
      Toggle({ type: ToggleType.Checkbox, isOn: this.task.done })
        .onChange((isOn: boolean) => {
          this.task.done = isOn; // 配合 @Observed 才能触发刷新
        })
    }
  }
}

3.2 状态管理选型建议

项目规模一旦超过 10 个页面,我强烈建议引入 AppStorage 或 LocalStorage 做跨页面共享,而不是层层传参。对于更复杂的场景,可以考虑官方推荐的 @ohos/arkui 状态管理方案或第三方状态库。

4. 页面路由与多设备适配

4.1 路由跳转

Stage 模型下推荐使用 router 或 Navigation。新项目我更推荐 Navigation,因为它天然支持分栏布局,对折叠屏、平板更友好。

import { router } from '@kit.ArkUI';

// 简单跳转
router.pushUrl({
  url: 'pages/DetailPage',
  params: { id: 1001 }
});

// 接收参数
const params = router.getParams() as Record<string, number>;
const id = params?.id;

4.2 折叠屏与多形态适配

这是本次活动的优先推荐方向之一,也是我实际花时间最多的部分。核心思路是用断点(breakpoint)驱动布局,而不是写死宽高。

@Entry
@Component
struct AdaptivePage {
  @StorageProp('currentBreakpoint') breakpoint: string = 'sm';

  build() {
    if (this.breakpoint === 'lg') {
      Row() {
        SideBar()
        ContentArea()
      }.width('100%')
    } else {
      Column() {
        ContentArea()
      }.width('100%')
    }
  }
}

配合 GridRow / GridCol 的响应式栅格,可以比较优雅地处理手机、折叠屏展开态、平板三种形态。踩坑点在于:折叠屏展开/折叠会触发窗口尺寸变化,需要在 onWindowStageCreate 中监听并更新断点,否则布局不会自动切换。

5. 网络请求与数据持久化

5.1 网络请求封装

鸿蒙推荐使用 @kit.NetworkKit 中的 http 模块。我封装了一个统一的请求工具,处理超时、错误码和 Token 注入:

import { http } from '@kit.NetworkKit';

export async function request<T>(url: string, method: http.RequestMethod = http.RequestMethod.GET, extraData?: object): Promise<T> {
  const httpRequest = http.createHttp();
  try {
    const resp = await httpRequest.request(url, {
      method,
      header: { 'Content-Type': 'application/json' },
      extraData: extraData ? JSON.stringify(extraData) : undefined,
      connectTimeout: 10000,
      readTimeout: 10000
    });
    if (resp.responseCode === 200) {
      return JSON.parse(resp.result as string) as T;
    }
    throw new Error(`HTTP ${resp.responseCode}`);
  } finally {
    httpRequest.destroy();
  }
}

重要提醒:httpRequest 用完必须 destroy(),否则会泄漏连接。我在压测时就是因为忘了这一步,导致请求数一多就卡死。

5.2 数据持久化

轻量数据用 preferences,结构化数据用 relationalStore(关系型数据库)。我这里的场景是缓存用户配置,用 preferences 足够:

import { preferences } from '@kit.ArkData';

const store = await preferences.getPreferences(getContext(this), 'user_config');
await store.put('token', 'xxx');
await store.flush(); // 必须 flush 才会落盘

又一个坑:put 之后不调用 flush(),数据只在内存里,应用重启就丢了。

6. 踩坑记录:那些让我加班到深夜的问题

6.1 真机调试白屏

现象:模拟器正常,真机启动白屏。排查后发现是 module.json5 中 abilities 的 srcEntry 路径写错,导致 UIAbility 没被正确加载。建议每次改完配置都重新 Sync 一次。

6.2 图片资源不显示

resources/base/media 下的图片,引用时必须带扩展名且不能有中文名。我一开始用了 icon-首页.png,编译不报错但运行时不显示,改成 icon_home.png 后正常。

6.3 列表卡顿

List 组件数据量大时卡顿,原因是没用 LazyForEach。改成懒加载后,1000 条数据滚动流畅:

List() {
  LazyForEach(this.dataSource, (item: TaskItem) => {
    ListItem() {
      TaskRow({ task: item })
    }
  }, (item: TaskItem) => item.id.toString())
}

6.4 上架审核被拒

第一次提审被拒,原因是隐私政策未在首次启动时弹窗告知,且申请了不必要的权限(定位)。整改方式:删除无用权限声明,补充隐私弹窗,二次提审通过。

7. 上架审核要点清单

结合我的实际经历,整理一份提审前自查清单:

  • 隐私政策:首次启动必须弹窗,且需用户主动同意后才能初始化 SDK。
  • 权限最小化:module.json5 中 requestPermissions 只声明真正用到的权限。
  • 应用信息完整:图标、名称、版本号、简介不能为空或占位。
  • 内容合规:无违规内容、无诱导分享、无外链跳转违规页面。
  • 兼容性:至少在手机 + 折叠屏/平板各测一遍。

8. 项目复盘:如果重来一次

回头看,这个项目最大的收获不是学会了某个 API,而是理解了鸿蒙「一次开发、多端部署」的设计哲学。如果重来一次,我会做三件事:

  1. 第一天就搭好分层结构,而不是边写边重构。
  2. 尽早接入真机调试,模拟器的坑掩盖了太多真实问题。
  3. 把断点适配做成基础设施,而不是最后临时补。

鸿蒙生态还在快速演进,文档和 API 都在持续更新。保持关注官方 Release Notes,比死记硬背 API 更重要。

9. 结语

以上就是我这次鸿蒙开发实战的完整记录。从环境搭建、状态管理、多设备适配,到网络、持久化、踩坑与上架,每一步都是真实经历。希望这些内容对正在做鸿蒙开发的你有一点帮助。

如果你也在写鸿蒙开发笔记,欢迎一起交流。开发路上,踩过的坑都是财富。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐