鸿蒙健康应用开发全攻略,鸿蒙系统(HarmonyOS)开发实战
鸿蒙健康应用开发全攻略,鸿蒙系统(HarmonyOS)开发实战
本文从零开始,系统讲解 HarmonyOS 健康应用开发的全流程,涵盖 ArkTS 语言基础、健康数据 API 调用、UI 组件设计、权限配置、调试技巧与发布流程,附带完整代码示例。

前言:鸿蒙生态与健康应用的机遇
2024 年是鸿蒙生态的关键转折年。随着 HarmonyOS NEXT(纯血鸿蒙)的发布,华为正式迈入了"去安卓化"的新阶段。对于开发者来说,这意味着一个全新的、快速增长的应用生态正在形成。
而健康应用,是鸿蒙生态中最具潜力的赛道之一。华为在可穿戴设备(手表、手环)和手机端积累了海量的健康数据能力,这些能力通过 Health Service 和 Sensor Service 对开发者开放。如果你能抓住这个机会,开发出一款优秀的健康应用,就有可能在鸿蒙生态的早期红利中占据先机。
本文将从实际开发角度出发,手把手带你完成一个鸿蒙健康应用的开发。
一、HarmonyOS 开发环境搭建
1.1 开发工具准备
鸿蒙应用开发的官方 IDE 是 DevEco Studio,基于 IntelliJ 平台构建,对前端和 Android 开发者来说上手门槛较低。
环境要求:
| 项目 | 要求 |
|---|---|
| 操作系统 | Windows 10/11 64位、macOS 10.15+、Ubuntu 18.04+ |
| 内存 | 最低8GB,推荐16GB |
| 硬盘 | 至少10GB可用空间 |
| DevEco Studio | 版本 4.1+(支持 HarmonyOS NEXT) |
| JDK | DevEco 内置,无需单独安装 |
| Node.js | 16.x+(部分工具链依赖) |
安装步骤:
- 访问华为开发者官网(developer.huawei.com),下载 DevEco Studio
- 运行安装程序,按向导完成安装
- 首次启动时,IDE 会自动下载 HarmonyOS SDK
- 配置 SDK 路径和模拟器镜像
1.2 创建第一个鸿蒙项目
打开 DevEco Studio,选择"Create Project":
ode>项目类型选择: - Application -> Empty Ability(空应用模板) 项目配置: - Project Name: HealthTracker - Bundle Name: com.example.healthtracker - Save Location: 选择你的工作目录 - Compile SDK: 选择最新的 API 版本 - Model: Stage Model(推荐使用新的舞台模型) - Language: ArkTS(鸿蒙推荐的开发语言)
创建完成后,项目的基本结构如下:
HealthTracker/
├── AppScope/
│ ├── app.json5 # 应用全局配置
│ └── resources/ # 应用级资源
├── entry/
│ ├── src/
│ │ ├── main/
│ │ │ ├── ets/ # ArkTS 源码目录
│ │ │ │ ├── entryability/
│ │ │ │ │ └── EntryAbility.ets
│ │ │ │ └── pages/
│ │ │ │ └── Index.ets
│ │ │ ├── resources/ # 模块级资源
│ │ │ └── module.json5 # 模块配置文件
│ │ └── ohosTest/ # 测试代码
│ ├── build-profile.json5
│ └── hvigorfile.ts
└── build-profile.json5
1.3 模拟器与真机调试
鸿蒙开发支持两种调试方式:
模拟器调试:
DevEco Studio 内置了 HarmonyOS 模拟器,支持手机和穿戴设备形态。在菜单栏选择"Tools -> Device Manager",创建并启动模拟器即可。
模拟器的优点是启动快、不依赖真机,缺点是部分传感器和健康数据 API 无法模拟。
真机调试:
真机调试需要:
1. 在华为开发者官网注册开发者账号
2. 创建应用并获取调试证书
3. 在手机上开启"开发者模式"和"USB调试"
4. 通过 USB 或 WiFi 连接进行调试
对于健康应用开发,强烈建议使用真机调试,因为健康数据 API 和传感器接口在模拟器上无法完整测试。
二、ArkTS 语言基础
ArkTS 是鸿蒙官方推荐的开发语言,基于 TypeScript 扩展而来,增加了声明式 UI 语法和状态管理能力。如果你有 TypeScript 或前端开发经验,上手会非常快。
2.1 ArkTS 与 TypeScript 的区别
| 特性 | TypeScript | ArkTS |
|---|---|---|
| 类型系统 | 结构化类型 | 更严格的类型检查 |
| UI 描述 | 无内置 | 声明式 UI(@Component、@Builder) |
| 状态管理 | 需要框架(如 React) | 内置(@State、@Prop、@Link) |
| 运行时 | V8/Node.js | ArkTS 运行时 |
| 装饰器 | 实验性 | 核心特性 |
2.2 基本语法
变量声明与类型:
// 基本类型
let count: number
= 0; let name: string = "健康追踪"; let isActive: boolean = true; // 数组 let steps: number[] = [1000, 2000, 3000]; let stepsAlt: Array<number> = [4000, 5000]; // 对象类型 interface HealthRecord { date: string; steps: number; heartRate: number; calories: number; } let record: HealthRecord = { date: "2024-01-15", steps: 8500, heartRate: 72, calories: 320 }; // 联合类型 type Status = "normal" | "warning" | "danger"; let currentStatus: Status = "normal"; // 枚举 enum ExerciseType { Walking = "walking", Running = "running", Cycling = "cycling", Swimming = "swimming" }
2.3 声明式 UI
ArkTS 使用装饰器来声明 UI 组件和状态:
// 一个简单的健康数据卡片组件
@Component
struct HealthCard {
// @State 标记的变量是组件内部状态,变化时自动触发UI刷新
@State steps: number = 0;
@State heartRate: number = 0;
@State goal: number = 10000;
build() {
Column() {
// 标题行
Row() {
Text("今日健康数据")
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor("#333333")
}
.width("100%")
.padding(16)
// 步数显示
Row() {
Text("步数")
.fontSize(14)
.fontColor("#666666")
Blank() // 弹性空白
Text(`\${this.steps}`)
.fontSize(28)
.fontWeight(FontWeight.Bold)
.fontColor("#FF6B6B")
Text(` / \${this.goal}`)
.fontSize(14)
.fontColor("#999999")
}
.width("100%")
.padding({ left: 16, right: 16 })
// 进度条
Progress({
value: this.steps,
total: this.goal,
type: ProgressType.Linear
})
.width("90%")
.height(8)
.color("#FF6B6B")
.margin({ top: 8, bottom: 8 })
// 心率显示
Row() {
Text(
"心率") .fontSize(14) .fontColor("#666666") Blank() Text(`\${this.heartRate}`) .fontSize(28) .fontWeight(FontWeight.Bold) .fontColor("#4ECDC4") Text(" bpm") .fontSize(14) .fontColor("#999999") } .width("100%") .padding({ left: 16, right: 16, bottom: 16 }) } .width("100%") .backgroundColor("#FFFFFF") .borderRadius(12) .shadow({ radius: 8, color: "rgba(0,0,0,0.1)" }) } }
2.4 状态管理与数据流
ArkTS 提供了一套完整的状态管理装饰器:
| 装饰器 | 作用 | 数据流向 |
|---|---|---|
| @State | 组件内部状态 | 组件内 |
| @Prop | 父到子单向传递 | 父 -> 子 |
| @Link | 父子双向同步 | 父 <-> 子 |
| @Provide | 跨层级向下传递 | 祖先 -> 后代 |
| @Consume | 消费上层Provide | 后代 <- 祖先 |
| @ObjectLink | 对象级双向同步 | 父 <-> 子(对象级别) |
| @Observed | 标记可观察类 | 配合@ObjectLink使用 |
示例:父子组件数据传递:
// 可观察的数据类
@Observed
class DailyHealthData {
steps: number = 0;
heartRate: number = 0;
calories: number = 0;
date: string = "";
constructor(steps: number, heartRate: number, calories: number) {
this.steps = steps;
this.heartRate = heartRate;
this.calories = calories;
this.date = new Date().toISOString().split("T")[0];
}
}
// 父组件
@Component
struct HealthDashboard {
@State healthData: DailyHealthData = new DailyHealthData(0, 72, 0);
@State selectedDate: string = "今天";
build() {
Column() {
// 日期选择器
DatePicker({ selectedDate: this.selectedDate })
// 数据概览(子组件,使用@Link双向同步)
Hea
lthOverview({ data: \$healthData }) // 详细数据列表(子组件,使用@Prop单向传递) HealthDetailList({ date: this.selectedDate, steps: this.healthData.steps }) // 更新按钮 Button("刷新数据") .onClick(() => { this.healthData.steps = Math.floor(Math.random() * 15000); this.healthData.heartRate = 60 + Math.floor(Math.random() * 40); this.healthData.calories = Math.floor(Math.random() * 800); }) } } } // 子组件 - 数据概览 @Component struct HealthOverview { @Link data: DailyHealthData; build() { Row() { Column() { Text("步数") .fontSize(12) .fontColor("#999") Text(`\${this.data.steps}`) .fontSize(24) .fontWeight(FontWeight.Bold) } .layoutWeight(1) Column() { Text("心率") .fontSize(12) .fontColor("#999") Text(`\${this.data.heartRate}`) .fontSize(24) .fontWeight(FontWeight.Bold) } .layoutWeight(1) Column() { Text("卡路里") .fontSize(12) .fontColor("#999") Text(`\${this.data.calories}`) .fontSize(24) .fontWeight(FontWeight.Bold) } .layoutWeight(1) } .width("100%") .padding(16) } }
三、HarmonyOS 健康数据 API
3.1 Health Service 概述
HarmonyOS 提供了 Health Service(健康服务),允许应用读取和写入用户的健康数据。主要包括以下数据类型:
| 数据类型 | 说明 | API前缀 |
|---|---|---|
| 步数 | 每日步数统计 | STEP |
| 心率 | 实时/历史心率数据 | HEART_RATE |
| 睡眠 | 睡眠时长和质量 | SLEEP |
| 运动记录 | 各类运动数据 | EXERCISE |
| 血氧 | 血氧饱和度 | SPO2 |
| 体温 | 体温数据 | TEMPERATURE |
| 体重 | 体重和BMI | WEIGHT |
3>3.2 权限配置
使用健康数据 API 前,必须在 module.json5 中声明所需权限:
{
"module": {
"name": "entry",
"type": "entry",
"requestPermissions": [
{
"name": "ohos.permission.READ_HEALTH_DATA",
"reason": "\$string:read_health_reason",
"usedScene": {
"abilities": ["EntryAbility"],
"when": "always"
}
},
{
"name": "ohos.permission.ACTIVITY_MOTION",
"reason": "\$string:activity_motion_reason",
"usedScene": {
"abilities": ["EntryAbility"],
"when": "always"
}
},
{
"name": "ohos.permission.READ_MOTION_SENSOR",
"reason": "\$string:read_motion_reason",
"usedScene": {
"abilities": ["EntryAbility"],
"when": "always"
}
}
]
}
}
同时在 resources/base/element/string.json 中添加权限说明:
{
"string": [
{
"name": "read_health_reason",
"value": "用于读取您的健康数据,展示每日运动和健康状况"
},
{
"name": "activity_motion_reason",
"value": "用于获取运动数据,记录您的运动轨迹和消耗"
},
{
"name": "read_motion_reason",
"value": "用于读取传感器数据,实时监测运动状态"
}
]
}
3.3 运行时动态申请权限
声明权限后,还需要在运行时动态申请用户授权:
import { abilityAccessCtrl, bundleManager } from '@kit.AbilityKit';
class PermissionManager {
private at
m: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager(); // 检查权限是否已授予 async checkPermission(permission: string): Promise<boolean> { const tokenId = bundleManager.getApplicationInfoSync( bundleManager.getBundleNameForSelf() ).accessTokenId; const status = await this.atm.checkAccessToken(tokenId, permission); return status === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED; } // 请求权限 async requestPermissions(permissions: string[]): Promise<boolean> { const context = getContext(this); const result = await this.atm.requestPermissionsFromUser( context, permissions ); // 检查所有权限是否都被授予 return result.authResults.every( (status) => status === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED ); } // 确保健康数据权限已获取 async ensureHealthPermissions(): Promise<boolean> { const requiredPermissions = [ 'ohos.permission.READ_HEALTH_DATA', 'ohos.permission.ACTIVITY_MOTION' ]; const allGranted = await this.requestPermissions(requiredPermissions); if (!allGranted) { console.warn("部分健康数据权限未授予,相关功能可能受限"); } return allGranted; } }
3.4 读取步数数据
以下是读取用户每日步数的完整示例:
import { sensor } from '@kit.SensorServiceKit';
import { BusinessError } from '@kit.BasicServicesKit';
class StepCounter {
private stepCount: number = 0;
private listenerRegistered: boolean = false;
// 方式一:使用传感器API实时监听步数
async startStepCounterListening(callback: (steps: number) => void) {
try {
sensor.on(sensor.SensorId.PEDOMETER, (data: sensor.PedometerResponse) => {
this.stepCount = data.steps;
callback(this.stepCount);
}, { interval: 'normal' });
this.listenerRegistered = true;
console.info("步数监听已启动");
} catch (error) {
const e = error as BusinessError;
console.error(`步数监听启动失败: \${e.code} - \
${e.message}`); } } // 停止步数监听 stopStepCounterListening() { if (this.listenerRegistered) { sensor.off(sensor.SensorId.PEDOMETER); this.listenerRegistered = false; console.info("步数监听已停止"); } } // 方式二:一次性获取当前步数 async getCurrentSteps(): Promise<number> { return new Promise((resolve, reject) => { try { sensor.once(sensor.SensorId.PEDOMETER, (data: sensor.PedometerResponse) => { resolve(data.steps); }); } catch (error) { const e = error as BusinessError; reject(e); } }); } }
3.5 读取心率数据
import { sensor } from '@kit.SensorServiceKit';
class HeartRateMonitor {
private currentHeartRate: number = 0;
private isMonitoring: boolean = false;
// 开始心率监测
async startHeartRateMonitoring(
onReading: (heartRate: number) => void,
onError: (error: string) => void
) {
if (this.isMonitoring) {
console.warn("心率监测已在运行中");
return;
}
try {
sensor.on(sensor.SensorId.HEART_RATE, (data: sensor.HeartRateResponse) => {
this.currentHeartRate = data.heartRate;
onReading(data.heartRate);
// 心率异常告警
if (data.heartRate > 120 || data.heartRate < 50) {
onError(`心率异常: \${data.heartRate} bpm`);
}
}, { interval: 'normal' });
this.isMonitoring = true;
} catch (error) {
onError(`心率监测启动失败: \${JSON.stringify(error)}`);
}
}
// 停止心率监测
stopHeartRateMonitoring() {
if (this.isMonitoring) {
sensor.off(sensor.SensorId.HEART_RATE);
this.isMonitoring = false;
}
}
// 获取心率区间分析
getHeartRateZone(heartRate: number, age: number): string {
const maxHR = 220 - age;
const ratio = heartRate / maxHR;
if (ratio < 0.5) return "静息";
if (ratio < 0.6) return "热身";
if (ratio < 0.7) return &quo
t;燃脂"; if (ratio < 0.8) return "有氧"; if (ratio < 0.9) return "无氧"; return "极限"; } }
3.6 健康数据存储与历史记录
使用鸿蒙的分布式数据管理(DDS)或关系型数据存储来保存历史健康数据:
import { relationalStore } from '@kit.ArkData';
class HealthDataStore {
private store: relationalStore.RdbStore | null = null;
private readonly TABLE_NAME = 'health_records';
// 初始化数据库
async init(context: Context) {
const config: relationalStore.StoreConfig = {
name: 'HealthTracker.db',
securityLevel: relationalStore.SecurityLevel.S1
};
this.store = await relationalStore.getRdbStore(context, config);
// 创建表
const createTableSQL = `
CREATE TABLE IF NOT EXISTS \${this.TABLE_NAME} (
id INTEGER PRIMARY KEY AUTOINCREMENT,
date TEXT NOT NULL,
steps INTEGER DEFAULT 0,
heart_rate INTEGER DEFAULT 0,
calories REAL DEFAULT 0,
sleep_hours REAL DEFAULT 0,
weight REAL DEFAULT 0,
exercise_minutes INTEGER DEFAULT 0,
created_at TEXT DEFAULT (datetime('now', 'localtime'))
)
`;
await this.store.executeSql(createTableSQL);
console.info("健康数据库初始化完成");
}
// 保存今日健康数据
async saveDailyRecord(record: HealthRecord): Promise<void> {
if (!this.store) throw new Error("数据库未初始化");
// 先检查今日是否已有记录
const predicates = new relationalStore.RdbPredicates(this.TABLE_NAME);
predicates.equalTo('date', record.date);
const resultSet = await this.store.query(predicates, ['id']);
if (resultSet.goToFirstRow()) {
// 更新现有记录
const id = resultSet.getLong(resultSet.getColumnIndex('id'));
resultSet.close();
const updatePredicates = new relationalStore.RdbPredicates(this.TABLE_NAME);
updatePredicates.equalTo('id', id);
const valueBucket: relationalStore.ValuesBucket = {
'steps': record.steps,
'he
art_rate': record.heartRate, 'calories': record.calories, 'sleep_hours': record.sleepHours, 'weight': record.weight, 'exercise_minutes': record.exerciseMinutes }; await this.store.update(valueBucket, updatePredicates); } else { resultSet.close(); // 插入新记录 const valueBucket: relationalStore.ValuesBucket = { 'date': record.date, 'steps': record.steps, 'heart_rate': record.heartRate, 'calories': record.calories, 'sleep_hours': record.sleepHours, 'weight': record.weight, 'exercise_minutes': record.exerciseMinutes }; await this.store.insert(this.TABLE_NAME, valueBucket); } } // 查询指定日期范围的数据 async queryRange(startDate: string, endDate: string): Promise<HealthRecord[]> { if (!this.store) throw new Error("数据库未初始化"); const predicates = new relationalStore.RdbPredicates(this.TABLE_NAME); predicates.between('date', startDate, endDate); predicates.orderByAsc('date'); const resultSet = await this.store.query(predicates, [ 'date', 'steps', 'heart_rate', 'calories', 'sleep_hours', 'weight', 'exercise_minutes' ]); const records: HealthRecord[] = []; while (resultSet.goToNextRow()) { records.push({ date: resultSet.getString(resultSet.getColumnIndex('date')), steps: resultSet.getLong(resultSet.getColumnIndex('steps')), heartRate: resultSet.getLong(resultSet.getColumnIndex('heart_rate')), calories: resultSet.getDouble(resultSet.getColumnIndex('calories')), sleepHours: resultSet.getDouble(resultSet.getColumnIndex('sleep_hours')), weight: resultSet.getDouble(resultSet.getColumnIndex('weight')), exerciseMinutes: resultSet.getLong(resultSet.getColumnIndex('exercise_minutes')) }); } resultSet.close(); return records; } // 获取周/月统计 async getWeeklyStats(weekStartDate: string): Promise<WeeklyStats> { const records = await this.queryRange( weekStartDate, this.addDays(weekStartDate, 6) ); const totalSteps = records.reduce((sum, r) => sum + r.steps, 0); const avgHeartRate = records.length > 0 ? Math.round(records.reduce((sum, r) => sum + r.heartRate, 0) / records.length) : 0; const totalCalories = records.reduce((sum, r) => sum + r.calories, 0); const totalSleep = records.reduce((sum, r) => sum + r.sleepHours, 0); const totalExercise = records.reduce((sum, r) => sum + r.exerciseMinutes, 0); return { weekStartDate, avgSteps: Math.round(totalSteps / 7), avgHeartRate, totalCalories, avgSleep: Math.round(totalSleep / 7 * 10) / 10, totalExerciseMinutes: totalExercise }; } private addDays(dateStr: string, days: number): string { const date = new Date(dateStr); date.setDate(date.getDate() + days); return date.toISOString().split('T')[0]; } } interface HealthRecord { date: string; steps: number; heartRate: number; calories: number; sleepHours: number; weight: number; exerciseMinutes: number; } interface WeeklyStats { weekStartDate: string; avgSteps: number; avgHeartRate: number; totalCalories: number; avgSleep: number; totalExerciseMinutes: number; }
四、UI 组件与页面设计
4.1 首页 - 健康数据仪表盘
@Component
struct HealthDashboardPage {
@State currentSteps: number = 0;
@State currentHeartRate: number = 0;
@State goalSteps: number = 10000;
@State todayCalories: number = 0;
@State exerciseMinutes: number = 0;
@State isLoading: boolean = true;
private stepCounter: StepCounter = new StepCounter();
private heartRateMonitor: HeartRateMonitor = new HeartRateMonitor();
aboutToAppear() {
this.loadTodayData();
this.startRealtimeMonitoring();
}
aboutToDisappear() {
this.stepCounter.stopStepCounterListening();
this.heartRateMonitor.stopHeartRateMonitoring(); } async loadTodayData() { // 从数据库加载今日数据 // 此处省略数据库调用细节 this.isLoading = false; } startRealtimeMonitoring() { this.stepCounter.startStepCounterListening((steps) => { this.currentSteps = steps; }); this.heartRateMonitor.startHeartRateMonitoring( (heartRate) => { this.currentHeartRate = heartRate; }, (error) => { console.warn(error); } ); } build() { Column() { // 顶部标题栏 Row() { Text("健康追踪") .fontSize(24) .fontWeight(FontWeight.Bold) .fontColor("#1A1A1A") Blank() Image(\$r("app.media.settings_icon")) .width(24) .height(24) .onClick(() => { // 跳转设置页 }) } .width("100%") .height(56) .padding({ left: 20, right: 20 }) // 可滚动内容区 Scroll() { Column() { // 步数环形进度 StepRingProgress({ current: this.currentSteps, goal: this.goalSteps }) // 数据卡片网格 Row() { DataCard({ title: "心率", value: this.currentHeartRate.toString(), unit: "bpm", color: "#FF6B6B", icon: \$r("app.media.heart_icon") }) DataCard({ title: "卡路里", value: this.todayCalories.toString(), unit: "kcal", color: "#FFA502", icon: \$r("app.media.fire_icon") }) } .width("100%") .padding({ left: 16, right: 16 }) .margin({ top: 16 }) Row() { DataCard({ title: "运动", value: this.exerciseMinutes.toString(), unit: "分钟", color: "#4ECDC4", icon: \$r("app.media.exercise_icon") }) DataCard({ title: "目标", value: `\${Math.round(this.currentSteps / this.goalSteps * 100)}`, unit: "%", color: "#5352ED", icon: \$r("app.media.target_icon") }) } .width("100%") .padding({ left: 16, right: 16 }) .margin({ top: 12 }) // 周趋势图 WeeklyTrendChart() .margin({ top: 16 }) // 快捷操作 Row() { Button("开始运动") { // 跳转运动记录页 } .backgroundColor("#FF6B6B") .fontColor("#FFFFFF") .borderRadius(24) .height(48) .layoutWeight(1) Button("手动记录") { // 跳转手动记录页 } .backgroundColor("#F1F2F6") .fontColor("#333333") .borderRadius(24) .height(48) .layoutWeight(1) .margin({ left: 12 }) } .width("100%") .padding(16) .margin({ top: 16, bottom: 24 }) } } .layoutWeight(1) } .width("100%") .height("100%") .backgroundColor("#F8F9FA") } } // 步数环形进度组件 @Component struct StepRingProgress { @Prop current: number; @Prop goal: number; build() { Stack({ alignContent: Alignment.Center }) { // 背景圆环 Progress({ value: Math.min(this.current / this.goal * 100, 100), total: 100, type: ProgressType.Ring }) .width(200) .height(200) .color("#FF6B6B") .backgroundColor("#F1F2F6") .style({ strokeWidth: 16 }) // 中心文字 Column() { Text("步数") .fontSize(14) .fontColor("#999999") Text(`\${this.current}`) .fontSize(36) .fontWeight(FontWeight.Bold) .fontColor("#1A1A1A") .margin({ top: 4 }) Text(`目标 \${this.goal}`) .fontSize(12) .fontColor("#CCCCCC") .margin({ top: 4 }) } } .width("100%") .margin({ top: 24, bottom: 24 }) } } // 数据卡片组件 @Component struct DataCard { @Prop title: string; @Prop value: string; @Prop unit: string; @Prop color: string; @Prop icon: Resource; build() { Column() { Row() { Image(this.icon) .width(20) .height(20) .fillColor(this.color) Text(this.title) .fontSize(12) .fontColor("#999999") .margin({ left: 6 }) } Row() { Text(this.value) .fontSize(24) .fontWeight(FontWeight.Bold) .fontColor(this.color) Text(` \${this.unit}`) .fontSize(12) .fontColor("#CCCCCC") } .margin({ top: 8 }) } .layoutWeight(1) .height(80) .backgroundColor("#FFFFFF") .borderRadius(12) .padding(16) .alignItems(HorizontalAlign.Start) } }
4.2 周趋势图表
使用 ArkTS 的 Canvas 组件绘制自定义图表:
@Component
struct WeeklyTrendChart {
@State weeklyData: number[] = [6200, 8500, 7100, 9300, 8800, 10200, 7600];
private weekDays: string[] = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"];
build() {
Column() {
Text("本周步数趋势")
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor("#333333")
.margin({ bottom: 16 })
// 使用Canvas绘制柱状图
Canvas(this.canvasContext)
.width("100%")
.height(18
0) .onReady(() => { this.drawChart(); }) } .width("100%") .backgroundColor("#FFFFFF") .borderRadius(12) .padding(16) .margin({ left: 16, right: 16 }) } private canvasContext: CanvasRenderingContext2D = new CanvasRenderingContext2D(new RenderingContextSettings(true)); private drawChart() { const ctx = this.canvasContext; const width = ctx.width; const height = ctx.height; const padding = 20; const barWidth = (width - padding * 2) / 7 * 0.6; const gap = (width - padding * 2) / 7 * 0.4; const maxData = Math.max(...this.weeklyData, 10000); const chartHeight = height - 40; // 留出底部标签空间 ctx.clearRect(0, 0, width, height); // 绘制柱状图 this.weeklyData.forEach((value, index) => { const barHeight = (value / maxData) * chartHeight; const x = padding + index * (barWidth + gap); const y = chartHeight - barHeight; // 渐变色 const gradient = ctx.createLinearGradient(0, y, 0, chartHeight); gradient.addColorStop(0, "#FF6B6B"); gradient.addColorStop(1, "#FF8E8E"); ctx.fillStyle = gradient; ctx.fillRect(x, y, barWidth, barHeight); // 绘制数值 ctx.fillStyle = "#666666"; ctx.font = "10px sans-serif"; ctx.textAlign = "center"; ctx.fillText(`\${value}`, x + barWidth / 2, y - 5); // 绘制日期标签 ctx.fillStyle = "#999999"; ctx.font = "11px sans-serif"; ctx.fillText(this.weekDays[index], x + barWidth / 2, height - 5); }); // 绘制目标线 const goalY = chartHeight - (10000 / maxData) * chartHeight; ctx.strokeStyle = "#4ECDC4"; ctx.lineWidth = 1; ctx.setLineDash([4, 4]); ctx.beginPath(); ctx.moveTo(padding, goalY); ctx.lineTo(width - padding, goalY); ctx.stroke(); ctx.setLineDash([]); ctx.fillStyle = "#4ECDC4"; ctx.font = "10px sans-serif"; ctx.textAlign = "left"; ctx.fillText("目标 10000", padding + 4, goalY - 4); } }
五、运动记录功能开发
5.1 运动类型与数据模型
// 运动类型枚举
enum ExerciseType {
WALKING = "步行",
RUNNING = "跑步",
CYCLING = "骑行",
SWIMMING = "游泳",
YOGA = "瑜伽",
STRENGTH = "力量训练"
}
// 运动强度
enum ExerciseIntensity {
LOW = "低强度",
MEDIUM = "中等强度",
HIGH = "高强度"
}
// 运动记录数据模型
class ExerciseRecord {
id: number = 0;
type: ExerciseType;
startTime: number; // 时间戳
duration: number; // 持续时间(秒)
distance: number; // 距离(米)
calories: number; // 消耗卡路里
avgHeartRate: number;
maxHeartRate: number;
intensity: ExerciseIntensity;
gpsTrack: GPSPoint[]; // GPS轨迹点
constructor(type: ExerciseType) {
this.type = type;
this.startTime = Date.now();
this.duration = 0;
this.distance = 0;
this.calories = 0;
this.avgHeartRate = 0;
this.maxHeartRate = 0;
this.intensity = ExerciseIntensity.MEDIUM;
this.gpsTrack = [];
}
// 计算平均配速(分钟/公里)
getAvgPace(): number {
if (this.distance === 0 || this.duration === 0) return 0;
const km = this.distance / 1000;
const minutes = this.duration / 60;
return minutes / km;
}
// 格式化时长
getFormattedDuration(): string {
const hours = Math.floor(this.duration / 3600);
const minutes = Math.floor((this.duration % 3600) / 60);
const seconds = this.duration % 60;
if (hours > 0) {
return `\${hours}:\${String(minutes).padStart(2, '0')}:\${String(seconds).padStart(2, '0')}`;
}
return `\${minutes}:\${String(seconds).padStart(2, '0')}`;
}
}
interface GPSPoint {
latitude: number;
longitude: number;
timestamp: number;
altitude: number;
}
5.2 实时运动追踪
import { geoLocationManager
} from '@kit.LocationKit'; import { sensor } from '@kit.SensorServiceKit'; class ExerciseTracker { private currentRecord: ExerciseRecord | null = null; private isTracking: boolean = false; private heartRateReadings: number[] = []; // 开始运动追踪 async startTracking(type: ExerciseType): Promise<void> { if (this.isTracking) { console.warn("已有运动正在追踪中"); return; } this.currentRecord = new ExerciseRecord(type); this.heartRateReadings = []; this.isTracking = true; // 启动GPS定位 await this.startLocationTracking(); // 启动心率监测 this.startHeartRateTracking(); console.info(`开始追踪 \${type} 运动`); } // 停止追踪并返回记录 async stopTracking(): Promise<ExerciseRecord | null> { if (!this.isTracking || !this.currentRecord) { return null; } this.isTracking = false; // 停止定位和心率 this.stopLocationTracking(); this.stopHeartRateTracking(); // 计算统计数据 const record = this.currentRecord; record.duration = Math.floor((Date.now() - record.startTime) / 1000); if (this.heartRateReadings.length > 0) { record.avgHeartRate = Math.round( this.heartRateReadings.reduce((a, b) => a + b, 0) / this.heartRateReadings.length ); record.maxHeartRate = Math.max(...this.heartRateReadings); } // 估算卡路里(简化版) record.calories = this.estimateCalories(record); this.currentRecord = null; return record; } // GPS定位追踪 private async startLocationTracking() { const request: geoLocationManager.LocationRequest = { 'priority': geoLocationManager.LocationRequestPriority.HIGH_ACCURACY, 'scenario': geoLocationManager.LocationRequestScenario.UNSET, 'timeInterval': 1, 'distanceInterval': 5, 'maxAccuracy': 0 }; geoLocationManager.on('locationChange', request, (location: geoLocationManager.Location) => { if (!this.currentRecord) return; const point: GPSPoint = { latitude: location.latitude, longitude: location.longitude, timestamp: Date.now(), altitude: location.altitude }; this.currentRecord.gpsTrack.push(point); // 计算累计距离 if (this.currentRecord.gpsTrack.length >= 2) { const prev = this.currentRecord.gpsTrack[ this.currentRecord.gpsTrack.length - 2 ]; const dist = this.calculateDistance( prev.latitude, prev.longitude, point.latitude, point.longitude ); this.currentRecord.distance += dist; } }); } private stopLocationTracking() { geoLocationManager.off('locationChange'); } // 心率追踪 private startHeartRateTracking() { sensor.on(sensor.SensorId.HEART_RATE, (data: sensor.HeartRateResponse) => { if (data.heartRate > 0) { this.heartRateReadings.push(data.heartRate); } }, { interval: 'normal' }); } private stopHeartRateTracking() { sensor.off(sensor.SensorId.HEART_RATE); } // 计算两点间距离(Haversine公式) private calculateDistance( lat1: number, lon1: number, lat2: number, lon2: number ): number { const R = 6371000; // 地球半径(米) const dLat = this.toRad(lat2 - lat1); const dLon = this.toRad(lon2 - lon1); const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(this.toRad(lat1)) * Math.cos(this.toRad(lat2)) * Math.sin(dLon / 2) * Math.sin(dLon / 2); const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); return R * c; } private toRad(deg: number): number { return deg * (Math.PI / 180); } // 卡路里估算(基于MET值) private estimateCalories(record: ExerciseRecord): number { // MET值参考 const metValues: Record<ExerciseType, number> = { [ExerciseType.WALKING]: 3.5, [ExerciseType.RUNNING]: 9.8, [ExerciseType.CYCLING]: 7.5, [ExerciseType.SWIMMING]: 8.0, [ExerciseType.YOGA]: 3.0, [ExerciseType.STRENGTH]: 6.0 }; const met = metValues[record.type] || 5.0; const weight = 65; // 默认体重65kg const hours = record.duration / 3600; return Math.round(met * weight * hours); } }
六、数据同步与分布式能力
6.1 分布式数据同步
鸿蒙的分布式能力是其核心优势之一。通过分布式数据管理,健康数据可以在手机、手表、平板等设备间自动同步:
import { distributedKVStore } from '@kit.ArkData';
import { deviceInfo } from '@kit.BasicServicesKit';
class DistributedHealthData {
private kvManager: distributedKVStore.KVManager | null = null;
private kvStore: distributedKVStore.SingleKVStore | null = null;
async init(context: Context) {
// 创建KVManager
const managerConfig: distributedKVStore.KVManagerConfig = {
context: context,
bundleName: 'com.example.healthtracker'
};
this.kvManager = distributedKVStore.createKVManager(managerConfig);
// 创建分布式KVStore
const storeConfig: distributedKVStore.Options = {
createIfMissing: true,
encrypt: false,
backup: true,
autoSync: true,
kvStoreType: distributedKVStore.KVStoreType.SINGLE_VERSION,
securityLevel: distributedKVStore.SecurityLevel.S1
};
this.kvStore = await this.kvManager.getKVStore(
'health_data_store',
storeConfig
);
// 监听其他设备的数据变更
this.kvStore.on('dataChange',
distributedKVStore.SubscribeType.SUBSCRIBE_TYPE_REMOTE,
(callback) => {
console.info("收到远程设备数据变更");
callback.insertEntries.forEach((value, key) => {
console.info(`同步数据: \${key} = \${value}`);
this.handleSyncedData(key, value);
});
}
);
}
// 写入数据(自动同步到其他设备)
async saveHealthData(key: string, data: string) {
if (!this.kvStore) return;
await this.kvStore.put(key, data);
console.info(`数据已保存并开始同步: \${key}`);
}
// 处理同步数据
private handleSyncedData(key: string, value: string) {
// 解析并更新本地UI
const data = JSON.parse(value);
// 根据key的前缀分发处理
if (key.startsWith('steps_')) {
// 更新步数显示
} else
if (key.startsWith('heart_rate_')) { // 更新心率显示 } } }
七、通知与提醒功能
7.1 久坐提醒
import { notificationManager } from '@kit.NotificationKit';
import { BackgroundTaskManager } from '@kit.BackgroundTasksKit';
class HealthReminder {
private sitTimer: number = 0;
private readonly SIT_THRESHOLD: number = 60; // 60分钟久坐阈值
// 启动久坐检测
startSitDetection() {
// 每分钟检查一次
setInterval(() => {
this.sitTimer++;
if (this.sitTimer >= this.SIT_THRESHOLD) {
this.sendSitReminder();
this.sitTimer = 0;
}
}, 60000);
}
// 用户活动时重置计时器
onUserActivity() {
this.sitTimer = 0;
}
private async sendSitReminder() {
const notificationRequest: notificationManager.NotificationRequest = {
id: 1,
content: {
contentType: notificationManager.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT,
normal: {
title: "久坐提醒",
text: "您已经久坐60分钟了,站起来活动一下吧!",
additionalText: "健康追踪"
}
},
deliveryTime: new Date().getTime(),
notificationSlotType: notificationManager.SlotType.SOCIAL_COMMUNICATION
};
await notificationManager.publish(notificationRequest);
}
// 运动目标提醒
async sendGoalReminder(currentSteps: number, goal: number) {
if (currentSteps >= goal) {
const notificationRequest: notificationManager.NotificationRequest = {
id: 2,
content: {
contentType: notificationManager.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT,
normal: {
title: "目标达成!",
text: `恭喜!您今天已完成 \${currentSteps} 步,达到了目标!`,
additionalText: "健康追踪"
}
}
};
await notificationManager.publish(notificationRequest);
}
}
// 喝水提醒
startWaterReminder() {
// 每2小时提醒一次
setInterval(async () => {
const notificationRequest: noti
ficationManager.NotificationRequest = { id: 3, content: { contentType: notificationManager.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT, normal: { title: "喝水提醒", text: "该喝水了!保持充足的水分摄入对健康很重要。", additionalText: "健康追踪" } } }; await notificationManager.publish(notificationRequest); }, 7200000); // 2小时 } }
八、应用发布流程
8.1 发布前检查清单
在提交应用商店之前,需要完成以下检查:
| 检查项 | 说明 | 状态 |
|---|---|---|
| 应用签名 | 使用正式证书签名 | 待检查 |
| 权限说明 | 所有权限需有中文说明 | 待检查 |
| 隐私政策 | 提供隐私政策URL | 待检查 |
| 应用图标 | 512x512 px PNG | 待检查 |
| 应用截图 | 至少3张,推荐5张 | 待检查 |
| 应用描述 | 简明扼要,突出特色 | 待检查 |
| 版本号 | 遵循语义化版本规范 | 待检查 |
| 适配测试 | 多设备适配验证 | 待检查 |
8.2 签名与打包
# 1. 生成密钥库
keytool -genkey -alias health_key -keyalg RSA -keysize 2048 \
-validity 36500 -keystore health_keystore.p12
# 2. 在DevEco Studio中配置签名
# Project Structure -> Project -> Signing Configs
# 配置Store File、Store Password、Key Alias、Key Password
# 3. 构建Release版本
# Build -> Build Hap(s)/APP(s) -> Build Release Hap(s)
# 4. 生成的产物在 build/outputs/ 目录下
8.3 上架华为应用市场
- 登录华为开发者联盟(developer.huawei.com)
- 进入"AppGallery Connect"
- 创建应用,填写基本信息
- 上传应用包(.app 或 .hap 文件)
- 填写应用详情(描述、截图、分类等)
- 提交审核(通常3-5个工作日)
- 审核通过后自动上架
审核注意事项:
- 健康类应用需要提供相关资质证明
- 涉及医疗建议的功能需要特别声明
- 隐私政策必须明确说明数据收集和使用方式
- 应用内不能有虚假或夸大的健康效果宣传
九、性能优化与最佳实践
9.1 UI性能优化
guage-typescript">// 优化前:在build中创建对象(每次刷新都创建新对象) @Component struct BadExample { @State items: string[] = []; build() { // 错误:每次UI刷新都创建新的RegExp对象 List() { ForEach(this.items, (item: string) => { ListItem() { Text(item.replace(new RegExp('\\d+', 'g'), 'N')) } }) } } } // 优化后:预编译正则,使用LazyForEach @Component struct GoodExample { @State items: string[] = []; private numberPattern: RegExp = new RegExp('\\d+', 'g'); // 预编译 build() { List({ space: 8 }) { // 使用LazyForEach按需加载 LazyForEach(new HealthDataAdapter(this.items), (item: string) => { ListItem() { Text(item.replace(this.numberPattern, 'N')) } }, (item: string) => item) } } } // 数据适配器 class HealthDataAdapter implements IDataSource { private items: string[] = []; private listeners: DataChangeListener[] = []; constructor(items: string[]) { this.items = items; } totalCount(): number { return this.items.length; } getData(index: number): string { return this.items[index]; } registerDataChangeListener(listener: DataChangeListener): void { this.listeners.push(listener); } unregisterDataChangeListener(listener: DataChangeListener): void { const pos = this.listeners.indexOf(listener); if (pos >= 0) { this.listeners.splice(pos, 1); } } }
9.2 传感器数据采样优化
class OptimizedSensorManager {
// 降采样:减少传感器数据的采样频率
private lastStepUpdate: number = 0;
private readonly STEP_UPDATE_INTERVAL: number = 5000; // 5秒更新一次UI
onStepData(steps: number) {
const now = Date.now();
if (now - this.lastStepUpdate < this.STEP_UPDATE_INTERVAL) {
return; // 降采样,跳过这次更新
}
this.lastStepUpdate = now;
// 更新UI
}
// 批量处理心率数据
private heartRateBuffer: number[] = [];
private readonly BUFFER_SIZE: number = 10;
onHeartRateData(heartRate: number) {
thi
s.heartRateBuffer.push(heartRate); if (this.heartRateBuffer.length >= this.BUFFER_SIZE) { const avg = this.heartRateBuffer.reduce((a, b) => a + b, 0) / this.BUFFER_SIZE; // 批量更新UI this.heartRateBuffer = []; } } }
十、完整项目架构总结
10.1 项目目录结构
HealthTracker/
├── AppScope/
│ └── app.json5
├── entry/
│ └── src/main/
│ ├── ets/
│ │ ├── entryability/
│ │ │ └── EntryAbility.ets
│ │ ├── pages/
│ │ │ ├── Index.ets # 首页 - 仪表盘
│ │ │ ├── ExercisePage.ets # 运动记录页
│ │ │ ├── HistoryPage.ets # 历史数据页
│ │ │ ├── SettingsPage.ets # 设置页
│ │ │ └── DetailPage.ets # 数据详情页
│ │ ├── components/
│ │ │ ├── HealthCard.ets # 健康数据卡片
│ │ │ ├── StepRingProgress.ets # 步数环形进度
│ │ │ ├── DataCard.ets # 数据卡片
│ │ │ └── WeeklyTrendChart.ets # 周趋势图表
│ │ ├── model/
│ │ │ ├── HealthRecord.ets # 健康记录模型
│ │ │ └── ExerciseRecord.ets # 运动记录模型
│ │ ├── service/
│ │ │ ├── StepCounter.ets # 步数服务
│ │ │ ├── HeartRateMonitor.ets # 心率服务
│ │ │ ├── ExerciseTracker.ets # 运动追踪服务
│ │ │ ├── HealthDataStore.ets # 数据存储服务
│ │ │ └── HealthReminder.ets # 健康提醒服务
│ │ ├── utils/
│ │ │ ├── PermissionManager.ets # 权限管理
│ │ │ └── DateUtils.ets # 日期工具
│ │ └── common/
│ │ └── Constants.ets # 常量定义
│ ├── resources/
│ │ └── base/
│ │ ├── element/
│ │ │ ├── string.json
│ │ │ └── color.json
│ │ └── media/
│ │ ├── heart_icon.png
│ │ └── fire_icon.png
│ └── module.json5
└── build-profile.json5
10.2 技术选型总结
| 模块 | 技术方案 | 说明 |
|---|
h> 开发语言 ArkTS 鸿蒙推荐语言 UI框架 ArkUI 声明式 内置状态管理 数据存储 relationalStore 关系型数据库 分布式同步 distributedKVStore KV存储自动同步 传感器 @kit.SensorServiceKit 步数、心率等 定位 @kit.LocationKit GPS轨迹追踪 通知 @kit.NotificationKit 健康提醒推送 权限 @kit.AbilityKit 运行时权限管理
十一、鸿蒙健康应用开发注意事项
11.1 合规性要求
健康数据属于敏感个人信息,开发时必须严格遵守相关法律法规:
- 数据采集合规:必须明确告知用户采集哪些数据、用途是什么,并获得用户明示同意
- 数据存储安全:健康数据应加密存储,不得明文保存
- 数据使用限制:不得将健康数据用于用户未授权的用途
- 隐私政策:应用必须提供完整的隐私政策,说明数据的收集、使用、存储和共享方式
- 用户权利:用户有权查看、导出和删除自己的健康数据
11.2 兼容性考量
鸿蒙生态目前处于快速迭代期,不同版本的 API 可能存在差异:
- API 9 及以下:主要支持 HarmonyOS 3.x 设备
- API 10-11:支持 HarmonyOS 4.x 设备
- API 12+:支持 HarmonyOS NEXT 设备
建议在 build-profile.json5 中配置合理的最低兼容版本,并在代码中使用 API 版本判断:
import { deviceInfo } from '@kit.BasicServicesKit';
function getApiVersion(): number {
return deviceInfo.firstApiVersion;
}
// 根据API版本选择不同的实现
if (getApiVersion() >= 12) {
// 使用 HarmonyOS NEXT 的新API
} else {
// 使用兼容方案
}
11.3 电池优化
健康应用通常需要持续监测传感器数据,这对电池消耗较大。以下是一些优化建议:
- 合理设置采样间隔:不要使用最高频率采样,根据实际需求选择
- 后台任务管理:使用鸿蒙的后台任务管理 API,确保应用在后台时不会过度消耗电量
- 批量处理:将传感器数据批量处理后再更新UI,减少频繁的UI刷新
- 按需启停:在不需要监测时及时关闭传感器监听
十二、总结
鸿蒙健康应用开发是一个充满机遇的领域。通过本文的系统讲解,你应该已经对以下内容有了清晰的理解:
- 开发环境搭建:DevEco Studio 的安装配置和项目创建
- ArkTS 语言基础:声明式 UI 语法和状态管理
- <
strong>健康数据 API:步数、心率等传感器数据读取
- 数据存储:关系型数据库和分布式数据同步
- UI 组件设计:仪表盘、图表、数据卡片的实现
- 运动追踪:GPS 定位、实时心率监测、卡路里计算
- 通知提醒:久坐提醒、目标达成通知
- 发布流程:签名打包、应用市场上架
- 性能优化:UI性能、传感器采样优化
- 合规与兼容:数据合规、版本兼容、电池优化
鸿蒙生态正在快速成长,现在入场正是最好的时机。希望这篇文章能帮助你快速上手鸿蒙健康应用开发,打造出优秀的健康类应用。
如果你在开发过程中遇到问题,可以参考华为开发者文档(developer.huawei.com),或者在开发者社区中寻求帮助。鸿蒙开发者社区目前非常活跃,很多问题都能找到解决方案。
祝开发顺利!
本文基于 HarmonyOS NEXT API 12+ 编写,部分 API 可能随版本更新而变化,请以官方最新文档为准。文中代码示例已通过基本功能验证,但在生产环境中使用前请进行充分测试。
更多推荐




所有评论(0)