
简介这是一份基于HarmonyOS鸿蒙系统的健康应用开发源码工程适合正在学习鸿蒙应用开发或希望快速搭建健康类App的开发者。工程围绕健康管理场景组织包含启动页、广告页、主页面、隐私政策弹窗等典型界面并涉及健康数据展示与基础交互逻辑通过阅读代码能较直观地了解鸿蒙应用的基本架构以及DevEco Studio的工程组织方式。资源包共106个文件主要类型包括ets页面文件、png图片资源、json/json5配置文件以及少量TypeScript、JavaScript辅助脚本整体体积仅1.85MB结构紧凑、便于按模块查阅。ets文件负责页面与组件逻辑png为界面图标与素材json/json5承担配置信息配合起来可快速定位所需功能点。目前已有528人学习下载适合在鸿蒙开发学习过程中结合参考尤其对希望实现运动步数、心率等健康数据采集与展示的开发者具有较好的实用价值。1. 从 hvigorw.bat 与 .ets 文件看健康应用工程骨架解开 HealthApp.zip 后散落着 hvigorw.bat、SplashPage.ets、AdvertisingPage.ets、MainPage.ets、Logger.ets 这些文件初看像是随手导出的工程碎片实际上是一条完成度很高的鸿蒙应用启动链路SplashPage 拉首屏AdvertisingPage 做推广位UserPrivacyDialog 在进主页前先处理隐私合规MainPage 才真正展示健康数据。这套结构对从零做鸿蒙健康应用的人来说比从 Hello World 搭起要实在得多也适合有 Android 迁移经验、想用 ArkTS 写第一版的人。本文直接拿这个 zip 当样本先讲清构建脚本和工程骨架怎么把它变成 HAP再拆页面生命周期、路由参数、日志测试和权限合规最后落到真机部署与调优上。每一步都有能照着敲的命令和代码。2. ArkTS 页面生命周期SplashPage、AdvertisingPage 与 MainPage 如何接力2.1 Entry 和 Component三个 .ets 文件共同的底座SplashPage、AdvertisingPage、MainPage 三个文件都声明了Entry Component这是 ArkTS 页面组件的标准写法。Entry表示该组件是页面入口可以被路由系统加载Component表示 struct 被编译成自定义组件。与 Android 的 Activity 不同ArkTS 页面本身仍然是一个组件生命周期由组件框架调度而不是由 ActivityTask 调度所以很多人第一次看会不习惯。// entry/src/main/ets/pages/SplashPage.ets Entry Component struct SplashPage { State countdown: number 3 aboutToAppear(): void { // 页面即将显示适合启动计时器 } build() { Column({ space: 12 }) { Text(健康应用启动中 ${this.countdown}s) .fontSize(18) Progress({ value: this.countdown, total: 3 }) .width(200) } .width(100%) .height(100%) .justifyContent(FlexAlign.Center) } }State是状态装饰器当countdown变化时build()里依赖它的 UI 会自动刷新。这段代码还用到Progress组件在启动页里做一个简单倒计时进度条能直观告诉用户“应用没有卡死”。实际项目中SplashPage 通常还承担初始化日志、读取本地配置、判断是否需要弹隐私对话框等任务所以aboutToAppear里不只有倒计时。2.2 aboutToAppear、onPageShow 与 onBackPress生命周期如何选ArkTS 页面有组件生命周期和页面生命周期两层。组件生命周期包括aboutToAppear、aboutToDisappear页面生命周期包括onPageShow、onPageHide、onBackPress。它们的差别直接影响健康应用的数据刷新策略。钩子触发时机典型用途aboutToAppearbuild 之前组件即将挂载初始化定时器、读取本地配置onPageShow页面每次显示时触发重新拉取健康数据、刷新主页onPageHide页面被覆盖或切后台时触发暂停埋点、保存草稿aboutToDisappear组件销毁前触发清理定时器、释放监听onBackPress用户按返回键时触发拦截“广告页误触返回”在健康应用里MainPage 的健康数据可能因手表同步、系统健康服务更新而改变所以不适合只在aboutToAppear里读一次而是放在onPageShow里读取。下面是一个常见写法Entry Component struct MainPage { State todaySteps: number 0 onPageShow(): void { this.loadTodaySteps() } loadTodaySteps(): void { // 从健康服务或本地数据库读取这里只做示意 this.todaySteps 12800 } build() { Column() { Text(今日步数${this.todaySteps}) .fontSize(24) } } }这样从广告页router.replaceUrl回主页、从后台切回前台时onPageShow都会触发数据始终是新鲜的。很多人犯的错是把读取逻辑放在aboutToAppear结果页面只刷新一次后续从后台回来数据是旧的。2.3 AdvertisingPage 里的定时器创建和销毁必须成对广告页一般有倒计时“5s 后跳过”并在倒计时结束或用户点击跳转按钮后进入 MainPage。定时器是这里最容易泄漏的资源必须把clearInterval放在aboutToDisappear和跳转前两个位置。Entry Component struct AdvertisingPage { State remain: number 5 private timerId: number -1 aboutToAppear(): void { this.timerId setInterval(() { if (this.remain 0) { if (this.timerId ! -1) { clearInterval(this.timerId) this.timerId -1 } router.replaceUrl({ url: pages/MainPage }) return } this.remain-- }, 1000) } aboutToDisappear(): void { if (this.timerId ! -1) { clearInterval(this.timerId) this.timerId -1 } } build() { Column() { Text(广告展示中 ${this.remain}s) Button(跳过 ${this.remain}s) .onClick(() { router.replaceUrl({ url: pages/MainPage }) }) } } }setInterval返回的是number在 ArkTS 的严格类型模式下不能直接给undefined所以初始化成-1清理时判断是否合法。这里强调两点一是倒计时归零后要跳转并清空定时器二是用户手动点击跳过时同样要clearInterval否则广告页销毁后定时器还在跑会引发内存泄漏和路由重复跳转。3. 路由参数传递与 CommonConstants 的工程化用法3.1 replaceUrl 与 pushUrl 的选择上一章代码里用了router.replaceUrl它和router.pushUrl的区别在于是否保留当前页面栈。启动页和广告页属于“一次性页面”用户进入主页后不应该还能返回到广告页所以必须用replaceUrl。如果业务需要从主页跳详情页并能返回则用pushUrl。router.pushUrl({ url: pages/DetailPage, params: { recordId: 10086, source: today } })params是路由参数可以是对象但 ArkTS 对对象字面量有类型约束。常规做法是把参数填充到已定义接口的实例中而不是直接写匿名对象interface DetailParams { recordId: number source: string } let params: DetailParams { recordId: 10086, source: today } router.pushUrl({ url: pages/DetailPage, params })目标页面通过router.getParams()读取const params router.getParams() as DetailParams if (params) { Text(记录 ${params.recordId} 来自 ${params.source}) }getParams()返回Object在 ArkTS 严格模式下必须做类型断言否则无法访问字段。as DetailParams是告诉编译器对象结构运行时若字段缺失会得到undefined所以使用前要判空。3.2 路由注册main_pages.json 不可或缺路由 URL 并不是文件路径的自动映射。鸿蒙应用需要在entry/src/main/resources/base/profile/main_pages.json中显式声明页面路径。很多新手把新页面.ets文件建好一运行就报“page not found”就是因为没有注册。{ src: [ pages/SplashPage, pages/AdvertisingPage, pages/MainPage, pages/UserPrivacyDialog ] }src数组中的字符串对应ets/目录下的相对路径不能带.ets后缀。如果新版本 DevEco Studio 使用的是route_map.json方式则在src/main/resources/base/profile/route_map.json中配置路由映射原理一致。这里的关键是路由注册文件与页面所在目录必须保持同步否则构建通过但运行时报路由错误。3.3 CommonConstants把魔法字符串关进笼子HealthApp.zip 里的CommonConstants.ets往往被人忽略实际上它是降低路由维护成本的关键文件。压缩包里多个页面都依赖路由 URL、倒计时时长、隐私键名与其在每个页面里硬编码不如集中放常量。export class CommonConstants { static readonly ROUTE_SPLASH_PAGE: string pages/SplashPage static readonly ROUTE_ADVERTISING_PAGE: string pages/AdvertisingPage static readonly ROUTE_MAIN_PAGE: string pages/MainPage static readonly ROUTE_PRIVACY_DIALOG: string pages/UserPrivacyDialog static readonly AD_SPLASH_DURATION: number 5 static readonly PRIVACY_AGREE_KEY: string privacy_agreed static readonly PRIVACY_PREF_NAME: string privacy_pref }引用时直接router.replaceUrl({ url: CommonConstants.ROUTE_MAIN_PAGE })。这样做的好处是当页面路径因目录重构变化时只需要改一个文件全局搜索ROUTE_MAIN_PAGE就能定位所有使用点。对于 5 个页面以下的 demo常量类看起来像是过度设计但健康应用一旦加入心率、睡眠、运动记录等模块页面数量会迅速超过 10 个到那时再回头整理路由字符串的代价远高于现在。3.4 Navigation 组件面向长方案的替代选择router是经典 API但鸿蒙主推的是NavigationNavDestination架构。Navigation在代码里直接配置页面映射不需要维护独立的main_pages.json并且天然支持路由拦截、转场动画和跨端迁移。如果你的目标是纯血鸿蒙HarmonyOS NEXT长期演进建议在项目一开始就引入Navigation把启动页、广告页、主页作为NavDestination放在同一个Navigation容器里。router更适合快速验证原型或维护存量页面栈大项目里最好不要两种混用否则页面返回栈会混乱。4. Logger 封装与 TestAbility日志埋点和自动化测试怎么做4.1 hilog 的 domain 与 tag先理解日志参数鸿蒙日志系统ohos.hilog要求每个日志都带domain和tag。domain是 32 位整型用于区分业务模块tag是字符串用于在日志里快速过滤。不设置 domain 的后果是多模块日志混在一起抓问题只能靠 grep效率很低。健康应用的 domain 建议按子系统划分比如 0x0001 给核心业务0x0002 给网络0x0003 给权限。4.2 Logger.ets 封装用 %{private}s 保护健康字段HealthApp.zip 里的Logger.ets一般是对 hilog 的薄封装。这样做的原因不只是少打字而是为了统一控制日志级别、统一格式化、避免%{private}s和%{public}s混用。健康数据属于敏感信息日志里如果直接打印心率、卡路里绝对不应该用%{public}s否则在调试日志里等于明文暴露。import hilog from ohos.hilog const DOMAIN: number 0x0001 const TAG: string HealthApp export class Logger { static info(message: string): void { hilog.info(DOMAIN, TAG, %{public}s, message) } static debug(message: string): void { hilog.debug(DOMAIN, TAG, %{public}s, message) } static warn(message: string): void { hilog.warn(DOMAIN, TAG, %{public}s, message) } static error(message: string): void { hilog.error(DOMAIN, TAG, %{public}s, message) } static privateInfo(message: string): void { hilog.info(DOMAIN, TAG, %{private}s, message) } }参数%{public}s表示该字符串会在日志中明文显示%{private}s表示在加密日志中会显示为private。真机调试时private字段需要通过 hdc 命令关闭隐私过滤才能看到这能防止敏感数据被普通hilog导出文件带走。建议默认封装里公开的是页面路由、按钮点击这类非敏感事件涉及步数、心率等健康指标的字段一律走privateInfo。4.3 用 Hypium 写 Ability.test.ets单元测试不流于形式Ability.test.ets是 DevEco Studio 自动生成的测试入口通常配合ohos/hypium框架使用。测试用例会注册到一个describe块中it定义具体断言。对于健康应用最好在测试里覆盖路由参数和隐私判断逻辑而不是只测一个恒真断言。import { describe, it, expect } from ohos/hypium import { CommonConstants } from ../commons/CommonConstants export default function abilityTest() { describe(HealthAppCommonTest, () { it(routerPathShouldNotBeEmpty, () { expect(CommonConstants.ROUTE_MAIN_PAGE) .assertNotEqual() }) it(adDurationShouldBePositive, () { expect(CommonConstants.AD_SPLASH_DURATION) .assertLarger(0) }) }) }assertLarger是 Hypium 断言器的数值比较方法。运行测试时如果CommonConstants.AD_SPLASH_DURATION被误改成 0广告页会出现无限倒计时测试能在开发阶段直接拦住这个回归。很多项目把Ability.test.ets当模板放着不动埋点逻辑和路由逻辑完全裸奔这是很可惜的。哪怕只针对CommonConstants写两个断言至少保证基础常量不会在重构中被“顺手改坏”。4.4 跑测试的两种方式IDE 和命令行DevEco Studio 中可以直接右键TestAbility.ets选择Run TestAbility来执行测试。命令行场景更适合 CI在项目根目录下执行./hvigorw.bat test或者指定模块测试./hvigorw.bat test --mode module -p moduleentrydefault -p isTesttrue执行后测试报告输出在entry/build/reports/tests目录下。这里要区分两个测试目录entry/src/test放纯 Java/JavaScript 单元测试entry/src/ohosTest放可以调用 HarmonyOS API 的测试。Ability.test.ets属于ohosTest因为它依赖 Ability 运行环境不能简单当纯函数测试跑。5. 健康数据权限与 UserPrivacyDialog从声明到本地存储5.1 为什么必须先弹隐私对话框再进主页健康应用会读取步数、心率、睡眠数据这些在《个人信息保护法》语境下属于敏感个人信息。合规路径是用户首次启动时通过UserPrivacyDialog明确告知“采集哪些数据、用于什么目的、是否共享给第三方”只有用户点同意后续代码才允许申请健康相关权限。很多开发者在SplashPage里直接跳主页等用户使用功能时才弹权限这在应用市场审核时可能被判定为“未明示收集规则”轻则拒绝上架重则要求整改下架。5.2 module.json5 中的 requestPermissions 声明权限管理分两步先在entry/src/main/module.json5里声明再在代码里动态申请。下面的配置是一个运动数据权限的典型写法{ module: { name: entry, requestPermissions: [ { name: ohos.permission.ACTIVITY_MOTION, reason: $string:reason_health, usedScene: { abilities: [EntryAbility], when: inuse } } ] } }reason必须指向字符串资源不能直接写中文字面量否则一些版本的工具链会校验失败。usedScene的when字段有三种取值inuse前台使用、always后台使用、never。健康数据读写通常写inuse除非明确需要后台记录。值得注意的是不是所有权限都能在requestPermissions里申请部分系统权限需要ACL方式申请这一点要查对应 API 版本的权限列表。5.3 requestPermissionsFromUser 动态授权仅在module.json5声明还不够鸿蒙 6 及以上版本强制要求运行时授权。代码里通过ohos.abilityAccessCtrl发起弹窗import abilityAccessCtrl from ohos.abilityAccessCtrl import common from ohos.app.ability.common async function requestMotionPermission(context: common.UIAbilityContext): Promiseboolean { const atManager abilityAccessCtrl.createAtManager() const permissions: ArrayPermissions [ohos.permission.ACTIVITY_MOTION] const result await atManager.requestPermissionsFromUser(context, permissions) if (result.authResults.length 0 result.authResults[0] 0) { return true } return false }authResults数组顺序与传入权限顺序一致0表示授权成功。这里要注意context的获取在页面组件里通过getContext(this) as common.UIAbilityContext拿到的 context 才能唤起系统权限弹窗如果用全局globalThis存的 context退出页面后可能成为悬空引用。健康应用中用户拒绝权限后不能反复弹窗骚扰通常需要引导到设置页但“跳设置页”在 HarmonyOS 里需要ohos.settings的能力不同版本 API 略有差异实现时要对照 SDK 文档。5.4 用 Preferences 记录同意状态不弹第二次UserPrivacyDialog本身只是一个页面或弹窗组件真正记住用户选择的是本地存储。这里使用轻量的 Preferences而不是关系型数据库因为只是一个布尔值。import preferences from ohos.data.preferences import common from ohos.app.ability.common import { CommonConstants } from ../commons/CommonConstants export async function hasAgreedPrivacy(context: common.Context): Promiseboolean { const pref await preferences.getPreferences(context, CommonConstants.PRIVACY_PREF_NAME) const agreed await pref.get(CommonConstants.PRIVACY_AGREE_KEY, false) return agreed as boolean } export async function setPrivacyAgreed(context: common.Context): Promisevoid { const pref await preferences.getPreferences(context, CommonConstants.PRIVACY_PREF_NAME) await pref.put(CommonConstants.PRIVACY_AGREE_KEY, true) await pref.flush() }flush()会立刻将数据落盘如果只是put不flush进程被杀后数据可能丢失。启动页aboutToAppear里先读hasAgreedPrivacy返回true就直接replaceUrl到主流程否则把UserPrivacyDialog作为当前页面用户同意后再执行路由。这样既满足首次进入的合规要求又不会让老用户每次启动都被弹窗打断。健康应用在后续版本中如果新增数据采集类型需要把“隐私版本号”也写入 Preferences老用户升级后仍需要重新确认新规则。6. 真机部署与调优把 HAP 装进鸿蒙设备6.1 命令行构建与签名配置拿到 zip 解压后不建议直接双击工程文件先在项目根目录打开终端确认构建工具可用./hvigorw.bat --version随后构建 HAP 包./hvigorw.bat assembleHap --mode module -p productdefault首次执行会自动下载hvigor相关依赖如果网络不稳定容易中断此时设置 DevEco Studio 的镜像仓库后重试即可。构建产物默认在entry/build/default/outputs/下生成.hap文件。真机安装前需要在File - Project Structure - Signing Configs里勾选Automatically generate signature并登录华为账号申请调试证书。没有签名的 HAP 无法安装到鸿蒙设备上。6.2 常见构建故障对照表症状常见原因应对手段hvigorw.bat 报 node 版本错误本机 Node.js 过旧或过新安装 Node.js 18 LTS并配置DEVECO_SDK_HOMESDK version 不匹配DevEco 与 API 版本不一致在build-profile.json5中降低compileSdkVersion或升级 IDEArkTS 报no-any错误代码里显式使用了any换成具体接口或Recordstring, Object路由找不到页面main_pages.json未注册新页面把新页面对应路径加入src数组真机无法安装签名过期或设备不在白名单重新生成签名并打开真机开发者调试模式6.3 真机验收清单HAP 安装到真机或模拟器后按三条路径回归检查一是启动链路确认 SplashPage 倒计时结束后进入广告页广告页倒计时结束进入 MainPage二是隐私链路首次安装启动出现 UserPrivacyDialog选择同意后进入主流程杀掉进程再次启动不再弹出三是权限链路拒绝运动权限后MainPage 步数区域显示“未授权”而不是直接崩溃。最后在真机终端里用日志过滤验证hdc shell hilog | grep HealthApp能看到HealthApp下的关键日志说明Logger.ets的 domain 和 tag 配置正确。把 hvigorw.bat 的构建产物装到真机后先用hilog | grep HealthApp观察日志输出再按广告页倒计时、隐私弹窗、主页数据三条路径回归一遍基本就能确认整条链路没有漏掉模块。本文还有配套的精品资源点击获取