☰
HarmonyOS SDK实战:从ArkUI声明式开发到Ability生命周期全记录
2026/10/20 14:57:57 网站建设 项目流程

三个多月前,我接到一个内部评估任务:下一个跨端产品到底要不要基于 HarmonyOS_SDK 做一套原生实现,还是继续沿用安卓包的简单适配。团队里没有一个人正经写过 HarmonyOS 代码,最初的共识非常一致——这不就是套壳安卓吗,让 Android 组的人顺手适配一下就行。说实话,当时的我也是这么想的。直到我把官方 IDE 装好、创建了一个空白 Ability 工程、把第一个页面跑起来,才发现这个判断错得离谱。

HarmonyOS SDK 并不是 Android SDK 的换皮。Ability 模型、ArkUI 声明式语法、Stage 规范、权限体系和打包链路,跟安卓的开发心智几乎没有重叠,反倒更像是一套用前端声明式思路重新设计过的移动开发体系。这篇文章以我这三个月的实战记录为主线,把环境搭建、工程初始化、核心架构理解、页面开发、权限与打包这些环节逐个拆开来讲,重点写官方文档不会主动告诉你的差异和坑位。如果你正准备做技术预研,或者已经被派去接手鸿蒙原生改造任务,这篇文章应该能帮你省下至少两周的摸索时间。

1. 技术选型预研:为什么我最终倾向原生实现

1.1 触发评估的产品背景

我们下一期的产品规划覆盖手机、平板,以及两个硬件合作伙伴的屏幕设备。按照老思路,方案无非两条:拿 Android APK 做机型适配,等兼容层成熟;或者引入跨端框架写一版 UI,期望一次编译、多端运行。这两条路都有隐患,安卓包在非安卓设备上的系统 API 行为一致性没人敢打保票,跨端框架对系统能力的封装深度又不够,特别是分布式数据、硬件协同这类鸿蒙侧重点强调的能力,第三方框架大多只做到了 UI 层对齐。

于是老板把评估任务交给了我这个新产品负责人,核心问题只有一个:HarmonyOS SDK 能不能撑起新增原生端的底座。初期我并没有抱太大期望,毕竟规划里没有给它留太多预算和时间,我的第一反应是先写一份 API 对比报告交差。但这个想法在我真正动手之后被彻底改写了。

1.2 颠覆认知的三个瞬间

第一击是 Ability 模型。安卓是 Activity + Fragment + Intent 那一套,鸿蒙原生是 UIAbility + WindowStage + AbilityContext,应用入口、窗口、上下文的关系完全不同。第二击是 UI 写法。ArkUI 是强约束的声明式语法,页面描述与状态绑定,你看不到 findViewById 那样的路径,所有变化都由框架自动同步到视图,开发者只负责改状态、描述结构。第三击是权限机制,动态授权的触发时机、申请流程跟安卓并非一一对应,API 也完全不同。

这三个瞬间让我意识到,拿安卓经验强行迁移是活不下去的。最终我把预算从"适配一版"改成了"做一次完整原型验证",也才有了后面这篇文章里的这些一手记录。

1.3 适合哪些人读

根据我的观察,三类人最适合读这篇内容:一是正在做技术选型的产品技术负责人,二是从 Android/iOS 转过来写鸿蒙原生的开发者,三是有前端经验、想快速切入声明式 UI 开发的程序员。前两类人建议重点看第 2 和第 3 部分,因为工程搭建和生命周期是最容易劝退的门槛;第三类人可以直接从第 4 部分的页面实战开始,ArkUI 对前端背景的同学友好程度超出预期。

2. 环境准备与第一个工程:三个环节的连环坑

2.1 工具链构成与前置配置

HarmonyOS SDK 的开发工具链由三块组成:官方 IDE、SDK Manager 和命令行辅助工具。SDK Manager 负责管理不同 API 版本的 SDK 组件,从 API 9 到 API 12 我都接触过,实测下来 API 12 的声明式能力最完整,但部分第三方库对它的适配还停留在 API 10 的水平,所以选型时不能只盯着最新版本,还要看依赖生态的跟进速度。

命令行工具常用的有两个:ohpm 类似前端的 npm,负责三方依赖安装;hvigor 负责工程构建。这两个工具平时隐藏在 IDE 内部,但排查构建问题的时候经常需要直接手敲命令,建议提前把它们的安装位置和版本号确认好,省得到时候两眼一抹黑。

提示:安装 IDE 和企业 SDK 时,不要放到带空格的路径下。hvigor 在处理资源文件时对路径相当敏感,这个莫名报错我排查了整整一个下午。

另外,SDK Manager 下载组件时对网络稳定性要求比较高。如果你的开发环境和组件仓库不在同一个网络区域,建议提前把镜像源配置好,等下载到一半断掉再重新拉取,会浪费大量时间。

2.2 工程目录结构:第一个认知门槛

新建 Empty Ability 工程之后,顶层会有 AppScope 和 entry 两个目录。AppScope 放应用级配置,entry 是唯一的模块目录,所有源码都集中在 entry/src/main 下,其中 ets 目录放页面代码,resources 目录放字符串、颜色、媒体资源。最容易忽略的是 entry/src/main/module.json5,这份文件相当于安卓的 Manifest,主入口 Ability、权限声明、设备类型支持全部写在这里。

我在第一个工程里犯过的最蠢错误,是把页面目录建在了 ets 外面,导致 IDE 找不到页面,而错误提示只给了一个含糊的编译失败。这种问题排查起来非常费劲,因为日志里根本不会告诉你目录结构不对,只能自己对照官方模板找差异。建议新建工程后先花十分钟把目录树捋一遍,搞清楚每个目录的语义再动手加代码。

2.3 签名、模拟器与真机的三角关系

HarmonyOS 工程默认不允许直接跑真机,必须先配置签名。自动签名需要登录厂商开发者账号,IDE 会自动生成调试证书;手动签名则需要准备证书文件并配置证书链。第一次跑真机时最常见的坑是证书过期时间配置不正确,导致安装失败,我的建议是调试阶段直接使用自动签名,别在证书上消耗精力。

调试路径上有三个选择:Previewer、模拟器和真机。Previewer 启动最快,适合单页面的 UI 快速验证,但它不支持全部系统能力,某些网络接口在 Previewer 上会直接抛异常;模拟器适合验证多页面跳转和大部分系统 API;真机才是最终标准。我的习惯是写 UI 用 Previewer,跑主流程用模拟器,发版前完整过一遍真机。

2.4 跑通第一个 Hello World 的正常流程

新建 Empty Ability 工程,等待依赖同步完成,选择 Previewer 启动,看到默认页面后再尝试修改一行文本、观察热刷新是否生效。这里有一个很多人会忽略的要点:如果热刷新没生效,优先去翻 IDE 底部的编译输出,因为 ArkTS 的编译期检查极其严格,一个类型问题就会阻塞整次刷新,而界面表现看起来只是"没变化"。先把编译错误清零,再谈界面优化。

3. Ability 与页面生命周期:被低估的核心门槛

3.1 从 Activity 思维迁移到 UIAbility 思维

安卓老手熟记的 Activity 生命周期是 onCreate、onResume、onPause 这套,而 Stage 模型下 UIAbility 的核心生命周期是 onCreate、onWindowStageCreate、onForeground、onBackground、onDestroy。一个关键差异在于:UIAbility 的 onCreate 并不代表界面已经可见,它只是能力对象创建完成;真正的界面加载发生在 onWindowStageCreate 里。

我第一次调 UI 不刷新,就是因为试图在 onWindowStageCreate 之前操作窗口对象。正确的习惯是:onCreate 阶段只做进程级初始化,例如创建单例、读取全局配置,把界面相关的逻辑留给窗口阶段。下面是我在项目里实际使用的骨架代码:

export default class EntryAbility extends UIAbility { onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void { // 这里不要碰窗口和 UI,只做进程级初始化 hilog.info(0x0000, 'EntryAbility', 'Ability created'); } onWindowStageCreate(windowStage: window.WindowStage): void { // 窗口创建完成,此时才可以加载页面 windowStage.loadContent('pages/Index'); } onForeground(): void { // 应用切到前台 } onBackground(): void { // 应用退到后台 } onDestroy(): void { // 释放资源 } }

3.2 窗口、页面、能力三者的关系

用一个生活化的类比来解释这三者:UIAbility 是餐厅的营业执照,WindowStage 是营业中的店面,页面是店面里的具体摊位。执照批下来(onCreate)不代表店面已经开张,只有窗口创建成功(onWindowStageCreate)之后,页面才有机会被加载。所以凡是依赖 UI 的初始化逻辑,都不该放在 onCreate 里,而应放在 onWindowStageCreate 或者更靠后的生命周期。

理解了这层关系之后,很多"为什么我的页面数据不对""为什么我拿不到窗口对象"之类的问题就迎刃而解了。这个部分也是团队里安卓老手最容易转不过来的地方,建议在写代码之前,先花时间把这套模型在脑子里过一遍。

3.3 路由方案:router 与 Navigation 的取舍

页面跳转方面,HarmonyOS SDK 提供了两套方案。router 模块提供 pushUrl、back、replace 这类命令式 API,简单直接,适合页面层级浅的简单应用;Navigation 是组件化方案,通过 NavPathStack 管理页面栈,适合底部 Tab 多、页面嵌套深的复杂应用。

实际评估之后我的建议是:原型和内部工具用 router 完全足够;商业产品直接上 Navigation。因为在 Navigation 体系里,系统返回手势、转场动画、跨层传参都能统一处理,后续维护成本低很多。这里有一个常见坑:router.pushUrl 的 url 必须和 pages 目录下的文件名完全对应,大小写差一个字母就找不到目标页面,而且错误要等到运行期才会暴露,非常隐蔽。

4. ArkUI 实战:一个待办页面从 0 到 1

4.1 先拆页面结构,再写代码

我习惯先做组件树拆解再动手写界面。一个待办事项页面,从上到下依次是标题区、输入区(TextInput + Button)、列表区(List + ForEach)。这种拆法在 ArkUI 下天然对应 Column 容器的嵌套,每一层职责清晰。实际代码是这样的:

@Entry @Component struct ToDoPage { @State tasks: string[] = []; @State inputText: string = ''; build() { Column({ space: 12 }) { Text('我的待办') .fontSize(24) .fontWeight(FontWeight.Bold) Row({ space: 8 }) { TextInput({ placeholder: '输入待办内容' }) .layoutWeight(1) .onChange((value: string) => { this.inputText = value; }) Button('添加') .onClick(() => { const text = this.inputText.trim(); if (text.length > 0) { this.tasks.push(text); this.inputText = ''; } }) } List({ space: 8 }) { ForEach( this.tasks, (task: string) => { ListItem() { Row() { Text(task).layoutWeight(1) Button('删除') .onClick(() => { const index = this.tasks.indexOf(task); if (index >= 0) { this.tasks.splice(index, 1); } }) } } }, (task: string) => task ) } .layoutWeight(1) } .padding(16) .width('100%') .height('100%') } }

这段代码里有几个细节值得展开:Column 默认按内容高度排布,列表区必须用 layoutWeight(1) 撑满剩余空间,否则 List 根本不会滚动;ForEach 的第三个参数是 key 生成器,这里直接用任务文本做 key,能保证新增和删除时列表只更新变化的条目,否则会退化成整个列表重建,性能和体验都会受影响。

4.2 状态管理装饰器怎么选

ArkUI 的响应式核心是状态装饰器。我高频使用的有五个:@State 管理组件内部状态,变化触发 re-render;@Prop 接收父组件传入的只读值,适合子组件展示场景;@Link 与父组件双向绑定,适合需要双向同步的父子状态;@Observed 配合 @ObjectLink 用于嵌套对象的深度监听;@Watch 则是在状态变化后触发回调,适合做联动逻辑。

新手最容易犯的错误,是把子组件需要展示的数据也声明成 @State,结果父组件改了值,子组件还抱着旧状态,看起来就是"UI 不刷新"。正确的姿势是:父组件持有唯一数据源,子组件需要的展示数据用 @Prop 传,需要回传改动时用 @Link 或者回调函数。这个原则和 React 的"单一数据源"思想是一致的,前端同学接受起来非常快。

4.3 列表项变成对象以后:@Observed 的正确用法

实际业务里待办项往往不止是字符串,还有完成状态、创建时间这些字段。数据结构升级成对象之后,直接 push 一个对象进数组再修改对象属性,UI 不一定感知得到。这时一般要给数据类加上 @Observed 装饰器,子组件里用 @ObjectLink 接收,并且要保证修改方式能被框架追踪。

这块是 ArkUI 状态管理中最容易绕晕的部分。我的建议是,从一开始就按"类 + @Observed"来组织数据模型,别走字符串捷径。等业务量起来之后再回头改造数据模型,成本会远高于一开始就按规范写。

4.4 数据持久化:Preferences 与关系型存储

页面代码只维护内存态,应用重启数据就丢了。轻量级数据我推荐使用 Preferences,API 风格接近安卓的 SharedPreferences,适合存用户配置、首选项这类键值数据。基本流程是:先通过 getPreferencesSync 拿到实例,再 put 数据,最后 flush 落盘。

import { preferences } from '@kit.ArkData'; let prefs = preferences.getPreferencesSync(context, { name: 'todoStore' }); prefs.putSync('lastCount', this.tasks.length); prefs.flush();

我在这里提醒一句:数据量一旦超过几百条、或者需要排序过滤查询,就老实上 relationalStore 关系型存储,使用体验接近简化版 SQLite。别图省事把列表数据也塞进 Preferences,后面做分页和筛选的时候你会非常痛苦。

4.5 日志与调试技巧

开发阶段我习惯在关键位置加 hilog 日志,但注意日志级别一定要分级,错误用 error、调试用 debug,别全用 info,否则刷屏会把你真正想看的错误信息冲掉。UI 不更新的问题,90% 出在装饰器加错或者数组操作方式不对上。ArkUI 能感知 push、splice 这类操作,但如果你先用临时变量整体替换数组再赋值给 @State,某些场景下反而会丢响应,这一点和 Vue2 的数组响应式限制有些相似。

5. 权限申请、打包上架与三个高频翻车点

5.1 运行时权限申请的正确姿势

HarmonyOS 的权限分为系统授权和用户授权两类。用户授权类权限必须在运行时向用户申请,我的做法分三步:先在 module.json5 的 requestPermissions 里声明具体权限,然后通过 abilityAccessCtrl.createAtManager() 拿到权限管理实例,最后在合适的时机用 requestPermissionsFromUser 发起弹窗申请。

关于申请时机,我踩过的坑是过早触发。在 Ability 的 onCreate 里发起申请,用户授权完回到页面时,数据链路还没准备好,容易造成状态丢失。更好的做法是等页面完成首帧渲染之后再请求,在授权回调里做后续处理,这样用户体验和数据流都更自然。

5.2 HAP 打包与签名校验

编译产物是 HAP 文件,在官方 IDE 里执行构建命令就能生成。打包本身不复杂,最折腾的是签名校验。我遇到的实际问题是 Debug 签名和 Release 签名的证书指纹不一致,导致安装时直接提示签名校验失败。解决方法是生成正式证书链之后,在 build-profile.json5 里把自动签名切换到手动签名,再重新打一次包。

另外要留意 targetSdkVersion 和 compatibleSdkVersion 这两个配置。compatible 决定应用能安装的最低系统版本,target 决定应用能使用的新 API 能力,两者差距过大的时候,要么低版本设备装不上,要么高版本系统上行为表现不一致,发版前一定反复核对。

5.3 高频翻车点汇总

我把预研阶段收集到的最典型问题整理成了一张表,方便你对照排查:

问题现象根本原因处理建议
编译提示 ArkTS 语法错误禁用了部分 TS 宽松写法关掉 any、按严格模式写代码
真机安装失败提示签名不对Debug 与 Release 证书不一致统一证书链,检查 build-profile
Previewer 里接口调用失败预览器不支持部分系统能力UI 用 Previewer,能力用模拟器或真机
列表超过二百条滚动掉帧ForEach 全量创建列表项换 LazyForEach 延迟创建
路由跳转找不到页面URL 大小写或路径不对核对 pages 目录文件名

ArkTS 的限制值得单独拿出来说。它的类型检查比 TypeScript 严格不少,any、非严格空值这些写法都会被编译器直接拦下来。从 JS/TS 转过来的人前期会很不适应,我的经验是让编译器多报几次错,比看十遍规范文档都管用。还有一个细节,ArkTS 禁止用 any 做类型标注,对象字面量必须显式声明接口,这是不少前端同事转过来的第一道坎。

5.4 性能优化的初步印象

预研阶段我做了两个简单的性能测试:页面冷启动和长列表滚动。冷启动的主要瓶颈在首帧渲染之前的初始化逻辑上,优化思路是把非必要的初始化全部延迟到 onForeground 之后再做。长列表滚动则要借助 LazyForEach 延迟创建列表项,普通 ForEach 在列表超过两百条时的帧率下降非常明显。相比跨端框架,原生方案的优势在于可控性更强,资源调度完全掌握在自己手里。

6. 收尾前分享几点实操体会

预研收尾时我们沉淀了一个内部评估模板,后来几个项目组复用下来都觉得顺手:第一周搭出一个跨 2 到 3 个页面的完整原型,验证基础 UI 和生命周期;第二周把权限、存储、网络这三类高频能力全部过一遍;第三周专门做真机测试和性能摸底。按这个节奏走,你得到的结论会比任何文档都可靠。

我个人最深的体会是,HarmonyOS SDK 并不是安卓的替代品,而是一套重新设计过的移动开发体系。它的 Ability 模型和 ArkUI 声明式语法有明确的学习门槛,但只要你愿意接受这套建模方式——Ability 管理生命周期、ArkUI 管理视图状态、编译期严格约束——写起来并不比安卓复杂,甚至在一些多设备协同的痛点上天然占优势。团队里如果有前端背景的同事,建议优先让他参与 UI 层开发,ArkUI 的声明式语法对他们非常友好,这可能是整个团队切入鸿蒙原生最平滑的一步。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询