30分钟上手健康类APP开发:HarmonyOS快速实战
·
30分钟上手健康类APP开发:HarmonyOS快速实战
零基础也能看懂的 Preferences 存储 + Canvas 图表入门教程
写在前面
想做一个健康追踪 APP,但不知道从哪开始?
这篇文章带你 30 分钟内完成一个完整的健康助手应用。不用担心,我会把每一步都讲清楚,包括:
- ✅ 如何存储用户的健康数据(步数、饮水、睡眠…)
- ✅ 如何用 Canvas 画柱状图
- ✅ 如何封装可复用的组件
- ✅ 如何设计底部导航的 4 个页面
准备好了吗?开始!
一、最终效果预览
做出来的 APP 长这样:
┌─────────────────────────────┐
│ 首页:健康仪表盘 │
│ - 今日步数、饮水、睡眠卡片 │
│ - 进度条显示目标达成率 │
│ - 健康小贴士轮播 │
├─────────────────────────────┤
│ 记录页:数据录入 │
│ - +/- 按钮调整数值 │
│ - emoji 选择心情 │
│ - 保存今日记录 │
├─────────────────────────────┤
│ 统计页:周趋势图表 │
│ - 步数柱状图 │
│ - 饮水+睡眠对比图 │
│ - 周平均统计 │
├─────────────────────────────┤
│ 设置页:个人目标 │
│ - 修改步数/饮水目标 │
│ - 清除数据 │
└─────────────────────────────┘
二、数据怎么存?Preferences 入门
2.1 什么是 Preferences?
Preferences 是 HarmonyOS 提供的轻量级存储方案,类似于 Android 的 SharedPreferences。
特点:
- 存储键值对(Key-Value)
- 数据写入磁盘,重启不丢失
- 适合存储用户设置、小型数据
2.2 基本用法
import { preferences } from '@kit.ArkData';
// 1. 获取 Preferences 实例
const store = await preferences.getPreferences(context, 'my_store');
// 2. 存数据
await store.put('user_name', '张三');
await store.flush(); // 重要!必须调用 flush 才会写入磁盘
// 3. 取数据
const name = await store.get('user_name', '默认值');
// 4. 清除所有数据
await store.clear();
await store.flush();
2.3 存储健康数据
健康数据结构:
interface DailyRecord {
date: string; // "2026-06-03"
steps: number; // 步数
water: number; // 饮水 ml
sleep: number; // 睡眠小时
calories: number; // 卡路里
mood: number; // 心情 1-5
note: string; // 备注
}
存储方式:把对象转成 JSON 字符串
// 存
const record = { date: '2026-06-03', steps: 8000, water: 1500, ... };
await store.put('record_2026-06-03', JSON.stringify(record));
await store.flush();
// 取
const json = await store.get('record_2026-06-03', '');
const record = JSON.parse(json) as DailyRecord;
三、封装存储管理器
每次都写 put/get/flush 太麻烦,封装一个管理器:
import { preferences } from '@kit.ArkData';
class StorageManager {
private store: preferences.Preferences | null = null;
// 初始化
async init(context: Context): Promise<void> {
this.store = await preferences.getPreferences(context, 'health_store');
}
// 保存记录
async saveRecord(record: DailyRecord): Promise<void> {
const json = JSON.stringify(record);
await this.store!.put('record_' + record.date, json);
await this.store!.flush();
}
// 加载记录
async loadRecord(date: string): Promise<DailyRecord | null> {
const json = await this.store!.get('record_' + date, '') as string;
return json ? JSON.parse(json) : null;
}
}
// 全局单例
const storageManager = new StorageManager();
export default storageManager;
使用:
// 初始化(在 EntryAbility 中)
await storageManager.init(this.context);
// 保存数据
await storageManager.saveRecord({
date: '2026-06-03',
steps: 8000,
water: 1500,
sleep: 7.5,
calories: 1800,
mood: 4,
note: '状态不错'
});
// 加载数据
const record = await storageManager.loadRecord('2026-06-03');
四、自定义组件:HealthCard
首页需要展示多个健康数据卡片,封装成组件:
4.1 组件代码
@Component
export struct HealthCard {
@Prop icon: string = '📊'; // emoji 图标
@Prop value: string = '0'; // 数值
@Prop unit: string = ''; // 单位
@Prop progress: number = 0; // 进度 0~1
@Prop color: Color = Color.Blue;
build() {
Column() {
Text(this.icon).fontSize(28)
Text(this.value).fontSize(24).fontWeight(FontWeight.Bold).fontColor(this.color)
Text(this.unit).fontSize(12).fontColor('#999')
// 进度条
Row() {
Row()
.width((this.progress * 100) + '%')
.height(4)
.backgroundColor(this.color)
.borderRadius(2)
}
.width('100%')
.height(4)
.backgroundColor('#E8E8E8')
.borderRadius(2)
.margin({ top: 8 })
}
.padding(12)
.backgroundColor(Color.White)
.borderRadius(12)
}
}
4.2 使用方式
Row() {
HealthCard({
icon: '🏃',
value: '6,842',
unit: '步',
progress: 0.68, // 6842 / 10000
color: Color.Blue
})
HealthCard({
icon: '💧',
value: '1200',
unit: 'ml',
progress: 0.6, // 1200 / 2000
color: Color.Blue
})
}
五、数值步进器:NumberStepper
记录页需要 +/- 按钮调整数值:
5.1 组件代码
@Component
export struct NumberStepper {
@Prop label: string = '';
@Prop unit: string = '';
@Prop step: number = 1;
@Link value: number; // 双向绑定
build() {
Row() {
Text(this.label).layoutWeight(1)
Button('-')
.width(36).height(36).borderRadius(18)
.onClick(() => { this.value = Math.max(0, this.value - this.step); })
Text(this.value.toString()).width(60).textAlign(TextAlign.Center)
Text(this.unit).fontSize(12).fontColor('#999')
Button('+')
.width(36).height(36).borderRadius(18)
.onClick(() => { this.value += this.step; })
}
.padding(12)
.backgroundColor(Color.White)
.borderRadius(12)
}
}
5.2 使用方式
@State steps: number = 0;
@State water: number = 0;
Column() {
NumberStepper({ label: '步数', unit: '步', step: 100, value: this.steps })
NumberStepper({ label: '饮水', unit: 'ml', step: 50, value: this.water })
}
六、Canvas 柱状图:从零开始
6.1 Canvas 基础
private ctx: CanvasRenderingContext2D = new CanvasRenderingContext2D();
Canvas(this.ctx)
.width(350)
.height(200)
.onReady(() => {
this.drawChart();
})
drawChart(): void {
const ctx = this.ctx;
// 清空画布
ctx.clearRect(0, 0, 350, 200);
// 画一个矩形
ctx.fillStyle = '#007AFF';
ctx.fillRect(50, 50, 30, 100);
// 画文字
ctx.fillStyle = '#666666';
ctx.font = '12px sans-serif';
ctx.fillText('周一', 50, 170);
}
6.2 绘制柱状图
drawStepChart(): void {
const ctx = this.ctx;
const data = [8000, 6500, 9200, 7800, 5500, 10200, 8900]; // 7天步数
const chartW = 300, chartH = 140;
const padL = 40, padT = 20;
const barW = 30, gap = 10;
// 找最大值
const maxVal = Math.max(...data);
// 画每个柱子
for (let i = 0; i < data.length; i++) {
const barH = (data[i] / maxVal) * chartH;
const x = padL + i * (barW + gap);
const y = padT + chartH - barH;
ctx.fillStyle = '#007AFF';
ctx.fillRect(x, y, barW, barH);
// 数值标签
ctx.fillStyle = '#007AFF';
ctx.font = '10px sans-serif';
ctx.textAlign = 'center';
ctx.fillText((data[i] / 1000).toFixed(1) + 'k', x + barW / 2, y - 4);
}
}
6.3 添加网格线和 Y 轴
// 网格线
ctx.strokeStyle = '#E8E8E8';
ctx.lineWidth = 0.5;
for (let i = 0; i <= 4; i++) {
const yy = padT + (chartH / 4) * i;
ctx.beginPath();
ctx.moveTo(padL, yy);
ctx.lineTo(padL + chartW, yy);
ctx.stroke();
// Y轴标签
const val = Math.round(maxVal * (1 - i / 4));
ctx.fillStyle = '#666';
ctx.textAlign = 'right';
ctx.fillText(val.toString(), padL - 6, yy + 4);
}
七、心情选择器
用 emoji 表示心情,点击选择:
@Component
export struct MoodSelector {
private moods = [
{ level: 1, emoji: '😫', text: '很差' },
{ level: 2, emoji: '😔', text: '较差' },
{ level: 3, emoji: '😐', text: '一般' },
{ level: 4, emoji: '😊', text: '不错' },
{ level: 5, emoji: '😄', text: '很棒' }
];
@Link currentMood: number;
build() {
Row() {
ForEach(this.moods, (item) => {
Column() {
Text(item.emoji)
.fontSize(32)
.opacity(item.level === this.currentMood ? 1 : 0.4)
Text(item.text)
.fontSize(10)
}
.padding(6)
.backgroundColor(item.level === this.currentMood ? '#1A007AFF' : Color.Transparent)
.borderRadius(12)
.onClick(() => { this.currentMood = item.level; })
})
}
}
}
八、底部 Tab 导航
4 个页面用 Tabs 组件:
@Entry
@Component
struct Index {
@State currentIndex: number = 0;
build() {
Tabs({ barPosition: BarPosition.End }) {
TabContent() { HomePage() }
.tabBar(this.tabBar('🏠', '首页', 0))
TabContent() { LogPage() }
.tabBar(this.tabBar('📝', '记录', 1))
TabContent() { StatsPage() }
.tabBar(this.tabBar('📊', '统计', 2))
TabContent() { ProfilePage() }
.tabBar(this.tabBar('⚙️', '设置', 3))
}
}
@Builder
tabBar(icon: string, label: string, index: number) {
Column() {
Text(icon).fontSize(22)
Text(label).fontSize(10)
.fontColor(this.currentIndex === index ? '#007AFF' : '#999')
}
}
}
九、首页:数据仪表盘
@Component
export struct HomePage {
@State todayRecord: DailyRecord | null = null;
aboutToAppear(): void {
this.loadData();
}
async loadData(): Promise<void> {
this.todayRecord = await storageManager.loadRecord(getTodayString());
}
build() {
Column() {
// 顶部问候
Row() {
Text('你好, 用户').fontSize(22).fontColor(Color.White)
}
.width('100%')
.padding(16)
.backgroundColor('#007AFF')
// 健康卡片
Row() {
HealthCard({
icon: '🏃',
value: this.todayRecord?.steps.toString() ?? '0',
unit: '步',
progress: (this.todayRecord?.steps ?? 0) / 10000
})
HealthCard({
icon: '💧',
value: this.todayRecord?.water.toString() ?? '0',
unit: 'ml',
progress: (this.todayRecord?.water ?? 0) / 2000
})
}
}
}
}
十、记录页:保存数据
@Component
export struct LogPage {
@State steps: number = 0;
@State water: number = 0;
@State mood: number = 3;
async saveRecord(): Promise<void> {
await storageManager.saveRecord({
date: getTodayString(),
steps: this.steps,
water: this.water,
sleep: 0,
calories: 0,
mood: this.mood,
note: ''
});
promptAction.showToast({ message: '保存成功!' });
}
build() {
Column() {
NumberStepper({ label: '步数', unit: '步', step: 100, value: this.steps })
NumberStepper({ label: '饮水', unit: 'ml', step: 50, value: this.water })
MoodSelector({ currentMood: this.mood })
Button('保存')
.onClick(() => { this.saveRecord(); })
}
}
}
十一、统计页:周图表
@Component
export struct StatsPage {
@State records: DailyRecord[] = [];
aboutToAppear(): void {
this.loadWeekData();
}
async loadWeekData(): Promise<void> {
const weekDates = getWeekDates(getWeekStart(getTodayString()));
this.records = await storageManager.loadRecordsInRange(weekDates);
}
build() {
Column() {
Canvas(this.ctx)
.width('100%')
.height(200)
.onReady(() => { this.drawChart(); })
}
}
drawChart(): void {
// 用 this.records 数据画柱状图
// ... 见第六节代码
}
}
十二、设置页:目标修改
@Component
export struct ProfilePage {
@State stepGoal: string = '10000';
@State waterGoal: string = '2000';
async saveSettings(): Promise<void> {
await storageManager.saveSettings({
userName: '用户',
stepGoal: parseInt(this.stepGoal),
waterGoal: parseInt(this.waterGoal),
sleepGoal: 8,
calorieGoal: 2000
});
}
build() {
Column() {
TextInput({ text: this.stepGoal, placeholder: '步数目标' })
.onChange((val) => { this.stepGoal = val; })
Button('保存')
.onClick(() => { this.saveSettings(); })
Button('清除所有数据')
.fontColor(Color.Red)
.onClick(() => {
AlertDialog.show({
title: '确认清除',
message: '此操作不可恢复',
secondaryButton: {
value: '确认',
action: () => { storageManager.clearAll(); }
}
});
})
}
}
}
十三、工具函数
常用的日期处理函数:
// 获取今天日期 "2026-06-03"
function getTodayString(): string {
const now = new Date();
const y = now.getFullYear();
const m = (now.getMonth() + 1).toString().padStart(2, '0');
const d = now.getDate().toString().padStart(2, '0');
return `${y}-${m}-${d}`;
}
// 获取本周一
function getWeekStart(dateStr: string): string {
const d = new Date(dateStr);
const day = d.getDay();
const diff = day === 0 ? -6 : 1 - day;
d.setDate(d.getDate() + diff);
return formatDate(d);
}
// 获取一周7天
function getWeekDates(startDate: string): string[] {
const start = new Date(startDate);
const dates: string[] = [];
for (let i = 0; i < 7; i++) {
const d = new Date(start);
d.setDate(start.getDate() + i);
dates.push(formatDate(d));
}
return dates;
}
十四、完整代码结构
ets/
├── components/
│ ├── HealthCard.ets # 健康卡片
│ └── CustomComponents.ets # 步进器/心情选择器
├── model/
│ └── HealthData.ets # 数据模型 + 工具函数
├── pages/
│ ├── Index.ets # 底部导航
│ ├── HomePage.ets # 首页
│ ├── LogPage.ets # 记录页
│ ├── StatsPage.ets # 统计页
│ └── ProfilePage.ets # 设置页
└── utils/
└── StorageManager.ets # 存储管理器
十五、运行效果

十六、扩展方向
- 添加更多健康指标(体重、血压)
- 折线图展示趋势
- 定时提醒喝水
- 数据导出 CSV
- 云同步
总结
这篇文章用最少的代码实现了:
| 功能 | 技术点 |
|---|---|
| 数据存储 | Preferences + JSON 序列化 |
| 健康卡片 | @Prop 进度条组件 |
| 数值录入 | @Link 双向绑定步进器 |
| 心情选择 | emoji + 点击高亮 |
| 周图表 | Canvas 柱状图 |
| 底部导航 | Tabs 组件 |
代码可以直接复制运行,有问题欢迎评论区讨论!
开发环境:DevEco Studio 5.0+ / SDK API 23
预计开发时间:30 分钟
更多推荐

所有评论(0)