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 分钟

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐