鸿蒙跨设备健康助手设计与实现

一、系统架构设计

基于HarmonyOS的分布式能力和传感器框架,我们设计了一个跨设备同步的健康助手应用,能够实时记录步数、同步健康数据并生成可视化图表。

https://example.com/health-assistant-arch.png

系统包含四大核心模块:

  1. ​传感器模块​​ - 使用@ohos.sensor获取步数数据
  2. ​数据存储模块​​ - 使用@ohos.data.storage本地持久化
  3. ​分布式同步模块​​ - 通过@ohos.distributedData实现多设备数据同步
  4. ​可视化模块​​ - 使用Canvas组件绘制统计图表

二、核心代码实现

1. 步数传感器服务(ArkTS)

// StepCounterService.ets
import sensor from '@ohos.sensor';
import distributedData from '@ohos.distributedData';

const STEP_COUNTER_SYNC_CHANNEL = 'step_counter_sync';

class StepCounterService {
  private static instance: StepCounterService = null;
  private dataManager: distributedData.DataManager;
  private stepCount: number = 0;
  private lastUpdateTime: number = 0;
  private listeners: StepListener[] = [];
  
  private constructor() {
    this.initDataManager();
    this.initStepCounter();
  }
  
  public static getInstance(): StepCounterService {
    if (!StepCounterService.instance) {
      StepCounterService.instance = new StepCounterService();
    }
    return StepCounterService.instance;
  }
  
  private initDataManager() {
    this.dataManager = distributedData.createDataManager({
      bundleName: 'com.example.healthassistant',
      area: distributedData.Area.GLOBAL
    });
    
    this.dataManager.registerDataListener(STEP_COUNTER_SYNC_CHANNEL, (data) => {
      this.handleSyncData(data);
    });
  }
  
  private initStepCounter() {
    try {
      sensor.on(sensor.SensorId.STEP_COUNTER, (data) => {
        this.handleStepData(data);
      });
      
      console.info('步数传感器初始化成功');
    } catch (err) {
      console.error('步数传感器初始化失败:', JSON.stringify(err));
    }
  }
  
  private handleStepData(data: sensor.SensorResponse) {
    const now = Date.now();
    const steps = data.steps;
    
    // 计算新增步数
    const newSteps = this.lastUpdateTime > 0 ? 
      steps - this.stepCount : 
      steps;
    
    this.stepCount = steps;
    this.lastUpdateTime = now;
    
    // 通知监听器
    this.listeners.forEach(listener => {
      listener.onStepChanged(newSteps, this.stepCount);
    });
    
    // 同步到其他设备
    this.syncStepData({
      timestamp: now,
      totalSteps: this.stepCount,
      deviceId: this.getLocalDeviceId()
    });
  }
  
  private syncStepData(stepData: StepData) {
    this.dataManager.syncData(STEP_COUNTER_SYNC_CHANNEL, {
      type: 'stepUpdate',
      data: stepData
    });
  }
  
  private handleSyncData(data: any) {
    if (data?.type === 'stepUpdate' && data.data) {
      const stepData: StepData = data.data;
      
      // 忽略自己设备同步的数据
      if (stepData.deviceId === this.getLocalDeviceId()) {
        return;
      }
      
      // 更新步数(简单合并,实际应用可能需要更复杂的逻辑)
      this.stepCount = Math.max(this.stepCount, stepData.totalSteps);
      this.lastUpdateTime = stepData.timestamp;
      
      this.listeners.forEach(listener => {
        listener.onStepChanged(0, this.stepCount); // 不触发新增步数通知
      });
    }
  }
  
  public getCurrentSteps(): number {
    return this.stepCount;
  }
  
  public addStepListener(listener: StepListener) {
    if (!this.listeners.includes(listener)) {
      this.listeners.push(listener);
    }
  }
  
  public removeStepListener(listener: StepListener) {
    this.listeners = this.listeners.filter(l => l !== listener);
  }
  
  private getLocalDeviceId(): string {
    // 实际实现需要获取设备ID
    return 'local_device';
  }
}

interface StepListener {
  onStepChanged(newSteps: number, totalSteps: number): void;
}

interface StepData {
  timestamp: number;
  totalSteps: number;
  deviceId: string;
}

export const stepCounterService = StepCounterService.getInstance();

2. 健康数据存储服务(ArkTS)

// HealthDataService.ets
import dataStorage from '@ohos.data.storage';
import distributedData from '@ohos.distributedData';

const HEALTH_DATA_SYNC_CHANNEL = 'health_data_sync';
const STORAGE_FILE = 'health_data';

class HealthDataService {
  private static instance: HealthDataService = null;
  private storage: dataStorage.Storage;
  private dataManager: distributedData.DataManager;
  
  private constructor() {
    this.initStorage();
    this.initDataManager();
  }
  
  public static getInstance(): HealthDataService {
    if (!HealthDataService.instance) {
      HealthDataService.instance = new HealthDataService();
    }
    return HealthDataService.instance;
  }
  
  private async initStorage() {
    try {
      this.storage = await dataStorage.getStorage(STORAGE_FILE);
      console.info('健康数据存储初始化成功');
    } catch (err) {
      console.error('健康数据存储初始化失败:', JSON.stringify(err));
    }
  }
  
  private initDataManager() {
    this.dataManager = distributedData.createDataManager({
      bundleName: 'com.example.healthassistant',
      area: distributedData.Area.GLOBAL
    });
    
    this.dataManager.registerDataListener(HEALTH_DATA_SYNC_CHANNEL, (data) => {
      this.handleSyncData(data);
    });
  }
  
  public async saveDailyData(date: string, data: DailyHealthData): Promise<void> {
    if (!this.storage) return;
    
    try {
      await this.storage.put(date, JSON.stringify(data));
      await this.storage.flush();
      
      // 同步到其他设备
      this.syncHealthData({
        date: date,
        data: data
      });
    } catch (err) {
      console.error('保存健康数据失败:', JSON.stringify(err));
    }
  }
  
  public async getDailyData(date: string): Promise<DailyHealthData | null> {
    if (!this.storage) return null;
    
    try {
      const data = await this.storage.get(date, '');
      return data ? JSON.parse(data) : null;
    } catch (err) {
      console.error('获取健康数据失败:', JSON.stringify(err));
      return null;
    }
  }
  
  public async getAllDates(): Promise<string[]> {
    if (!this.storage) return [];
    
    try {
      return await this.storage.listAllKeys();
    } catch (err) {
      console.error('获取日期列表失败:', JSON.stringify(err));
      return [];
    }
  }
  
  private syncHealthData(healthData: SyncHealthData) {
    this.dataManager.syncData(HEALTH_DATA_SYNC_CHANNEL, {
      type: 'healthDataUpdate',
      data: healthData
    });
  }
  
  private handleSyncData(data: any) {
    if (data?.type === 'healthDataUpdate' && data.data) {
      const syncData: SyncHealthData = data.data;
      
      // 保存同步过来的数据
      this.saveDailyData(syncData.date, syncData.data).catch(err => {
        console.error('同步保存健康数据失败:', JSON.stringify(err));
      });
    }
  }
}

interface DailyHealthData {
  steps: number;
  distance: number; // 米
  calories: number; // 千卡
  heartRate?: number; // 可选心率数据
}

interface SyncHealthData {
  date: string;
  data: DailyHealthData;
}

export const healthDataService = HealthDataService.getInstance();

3. 健康数据可视化组件(ArkUI)

// HealthChart.ets
@Component
export struct HealthChart {
  @State chartData: ChartData[] = [];
  @State selectedDate: string = '';
  
  private stepService = stepCounterService;
  private healthService = healthDataService;
  
  aboutToAppear() {
    this.loadChartData();
    
    // 监听步数变化
    this.stepService.addStepListener({
      onStepChanged: (newSteps, totalSteps) => {
        this.updateTodaySteps(totalSteps);
      }
    });
  }
  
  aboutToDisappear() {
    this.stepService.removeStepListener({
      onStepChanged: (newSteps, totalSteps) => {
        this.updateTodaySteps(totalSteps);
      }
    });
  }
  
  build() {
    Column() {
      // 日期选择器
      DatePicker({
        start: '2020-01-01',
        end: '2100-12-31',
        selected: this.selectedDate || new Date().toISOString().split('T')[0]
      })
      .onChange((value: DatePickerResult) => {
        this.selectedDate = `${value.year}-${value.month}-${value.day}`;
        this.loadChartData();
      })
      
      // 图表容器
      Stack() {
        // 背景网格
        this.buildGrid()
        
        // 柱状图
        ForEach(this.chartData, (item) => {
          this.buildBar(item)
        })
        
        // 标签
        this.buildLabels()
      }
      .height(300)
      .margin({ top: 20 })
    }
  }
  
  @Builder
  buildGrid() {
    // 水平网格线
    ForEach([0, 1, 2, 3, 4], (i) => {
      Line()
        .width('100%')
        .height(1)
        .backgroundColor('#E0E0E0')
        .position({ x: 0, y: i * 60 })
    })
  }
  
  @Builder
  buildBar(item: ChartData) {
    const barHeight = item.value / this.getMaxValue() * 240;
    
    Column() {
      // 柱状条
      Column()
        .width(30)
        .height(barHeight)
        .backgroundColor('#4CAF50')
        .borderRadius(4)
      
      // 数据标签
      Text(item.value.toString())
        .fontSize(12)
        .margin({ top: 4 })
    }
    .position({ x: item.position * 80 + 40, y: 240 - barHeight })
  }
  
  @Builder
  buildLabels() {
    // X轴标签
    ForEach(this.chartData, (item) => {
      Text(item.label)
        .fontSize(12)
        .position({ x: item.position * 80 + 40, y: 250 })
    })
  }
  
  private async loadChartData() {
    const date = this.selectedDate || new Date().toISOString().split('T')[0];
    const data = await this.healthService.getDailyData(date);
    
    if (data) {
      this.chartData = [
        { label: '步数', value: data.steps, position: 0 },
        { label: '距离', value: data.distance, position: 1 },
        { label: '卡路里', value: data.calories, position: 2 }
      ];
    } else {
      // 默认数据
      this.chartData = [
        { label: '步数', value: 0, position: 0 },
        { label: '距离', value: 0, position: 1 },
        { label: '卡路里', value: 0, position: 2 }
      ];
    }
  }
  
  private async updateTodaySteps(steps: number) {
    const today = new Date().toISOString().split('T')[0];
    if (this.selectedDate === today) {
      const data = await this.healthService.getDailyData(today) || {
        steps: 0,
        distance: 0,
        calories: 0
      };
      
      data.steps = steps;
      data.distance = this.calculateDistance(steps);
      data.calories = this.calculateCalories(steps);
      
      this.chartData = [
        { label: '步数', value: data.steps, position: 0 },
        { label: '距离', value: data.distance, position: 1 },
        { label: '卡路里', value: data.calories, position: 2 }
      ];
      
      // 保存更新后的数据
      await this.healthService.saveDailyData(today, data);
    }
  }
  
  private calculateDistance(steps: number): number {
    // 假设步长0.7米
    return steps * 0.7;
  }
  
  private calculateCalories(steps: number): number {
    // 简单计算:0.04千卡/步
    return steps * 0.04;
  }
  
  private getMaxValue(): number {
    return Math.max(...this.chartData.map(item => item.value), 1);
  }
}

interface ChartData {
  label: string;
  value: number;
  position: number;
}

三、主界面整合

1. 健康助手主页面

// MainPage.ets
import { stepCounterService } from './StepCounterService';
import { healthDataService } from './HealthDataService';

@Entry
@Component
struct MainPage {
  @State currentSteps: number = 0;
  @State todayData: DailyHealthData | null = null;
  @State deviceList: DeviceInfo[] = [];
  
  private stepListener: StepListener = {
    onStepChanged: (newSteps, totalSteps) => {
      this.currentSteps = totalSteps;
      this.updateTodayData(totalSteps);
    }
  };
  
  aboutToAppear() {
    // 初始化步数监听
    stepCounterService.addStepListener(this.stepListener);
    this.currentSteps = stepCounterService.getCurrentSteps();
    
    // 加载今日数据
    this.loadTodayData();
    
    // 加载设备列表
    this.loadDeviceList();
  }
  
  aboutToDisappear() {
    stepCounterService.removeStepListener(this.stepListener);
  }
  
  build() {
    Column() {
      // 头部状态
      this.buildHeader()
      
      // 步数卡片
      this.buildStepCard()
      
      // 健康图表
      HealthChart()
        .margin({ top: 20 })
      
      // 设备列表
      this.buildDeviceList()
    }
    .padding(20)
  }
  
  @Builder
  buildHeader() {
    Row() {
      Text('健康助手')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
      
      Button('同步')
        .onClick(() => {
          this.syncAllData();
        })
    }
    .width('100%')
    .justifyContent(FlexAlign.SpaceBetween)
  }
  
  @Builder
  buildStepCard() {
    Column() {
      Text('今日步数')
        .fontSize(16)
      
      Text(this.currentSteps.toString())
        .fontSize(36)
        .fontWeight(FontWeight.Bold)
        .margin({ top: 8 })
      
      if (this.todayData) {
        Row() {
          Text(`距离: ${this.todayData.distance.toFixed(1)}米`)
          Text(`卡路里: ${this.todayData.calories.toFixed(1)}千卡`)
            .margin({ left: 20 })
        }
        .margin({ top: 12 })
      }
    }
    .width('100%')
    .padding(20)
    .backgroundColor('#FFFFFF')
    .borderRadius(12)
  }
  
  @Builder
  buildDeviceList() {
    Column() {
      Text('已连接设备')
        .fontSize(18)
        .margin({ bottom: 10 })
      
      ForEach(this.deviceList, (device) => {
        Row() {
          Image(device.icon)
            .width(40)
            .height(40)
          
          Column() {
            Text(device.name)
            Text(`步数: ${device.steps}`)
              .fontSize(12)
              .margin({ top: 4 })
          }
          .margin({ left: 10 })
        }
        .width('100%')
        .padding(10)
        .backgroundColor('#F5F5F5')
        .borderRadius(8)
        .margin({ bottom: 8 })
      })
    }
    .margin({ top: 20 })
  }
  
  private async loadTodayData() {
    const today = new Date().toISOString().split('T')[0];
    this.todayData = await healthDataService.getDailyData(today);
    
    if (!this.todayData) {
      this.todayData = {
        steps: this.currentSteps,
        distance: this.calculateDistance(this.currentSteps),
        calories: this.calculateCalories(this.currentSteps)
      };
      await healthDataService.saveDailyData(today, this.todayData);
    }
  }
  
  private async updateTodayData(steps: number) {
    const today = new Date().toISOString().split('T')[0];
    this.todayData = {
      steps: steps,
      distance: this.calculateDistance(steps),
      calories: this.calculateCalories(steps)
    };
    await healthDataService.saveDailyData(today, this.todayData);
  }
  
  private calculateDistance(steps: number): number {
    return steps * 0.7;
  }
  
  private calculateCalories(steps: number): number {
    return steps * 0.04;
  }
  
  private async loadDeviceList() {
    // 模拟设备数据
    this.deviceList = [
      { id: '1', name: '我的手机', steps: 8520, icon: $r('app.media.phone') },
      { id: '2', name: '智能手表', steps: 7680, icon: $r('app.media.watch') }
    ];
  }
  
  private syncAllData() {
    // 同步今日数据
    const today = new Date().toISOString().split('T')[0];
    if (this.todayData) {
      healthDataService.saveDailyData(today, this.todayData);
    }
    
    // 同步步数
    stepCounterService.syncStepData({
      timestamp: Date.now(),
      totalSteps: this.currentSteps,
      deviceId: 'local_device'
    });
    
    promptAction.showToast({
      message: '数据已同步',
      duration: 2000
    });
  }
}

interface DeviceInfo {
  id: string;
  name: string;
  steps: number;
  icon: Resource;
}

四、权限配置与模块声明

1. 配置文件

// module.json5
{
  "module": {
    "requestPermissions": [
      {
        "name": "ohos.permission.ACTIVITY_MOTION",
        "reason": "访问步数传感器"
      },
      {
        "name": "ohos.permission.DISTRIBUTED_DATASYNC",
        "reason": "跨设备数据同步"
      },
      {
        "name": "ohos.permission.HEALTH_DATA",
        "reason": "访问健康数据"
      }
    ],
    "abilities": [
      {
        "name": "MainAbility",
        "type": "page",
        "visible": true
      }
    ],
    "distributedNotification": {
      "scenarios": [
        {
          "name": "health_data_sync",
          "value": "health_assistant"
        }
      ]
    }
  }
}

2. 资源文件

<!-- resources/base/element/string.json -->
{
  "string": [
    {
      "name": "app_name",
      "value": "健康助手"
    },
    {
      "name": "step_counter_title",
      "value": "今日步数"
    },
    {
      "name": "distance_label",
      "value": "距离"
    },
    {
      "name": "calories_label",
      "value": "卡路里"
    }
  ]
}

五、跨设备同步优化

1. 增量同步策略

// EnhancedHealthSync.ets
class EnhancedHealthSync {
  private lastSyncTime: number = 0;
  
  async syncIncrementalData() {
    const now = Date.now();
    const lastSync = this.lastSyncTime;
    this.lastSyncTime = now;
    
    // 获取需要同步的日期范围
    const dates = await this.getDatesToSync(lastSync);
    
    // 分批同步
    const batchSize = 5;
    for (let i = 0; i < dates.length; i += batchSize) {
      const batch = dates.slice(i, i + batchSize);
      await this.syncBatchData(batch);
    }
  }
  
  private async getDatesToSync(lastSync: number): Promise<string[]> {
    const allDates = await healthDataService.getAllDates();
    
    if (lastSync === 0) {
      return allDates; // 首次同步全部数据
    }
    
    // 只同步修改时间晚于上次同步时间的数据
    const datesToSync: string[] = [];
    for (const date of allDates) {
      const data = await healthDataService.getDailyData(date);
      if (data && data.timestamp && data.timestamp > lastSync) {
        datesToSync.push(date);
      }
    }
    
    return datesToSync;
  }
  
  private async syncBatchData(dates: string[]) {
    const batchData: SyncHealthData[] = [];
    
    for (const date of dates) {
      const data = await healthDataService.getDailyData(date);
      if (data) {
        batchData.push({
          date: date,
          data: data,
          timestamp: Date.now()
        });
      }
    }
    
    if (batchData.length > 0) {
      distributedData.syncData(HEALTH_DATA_SYNC_CHANNEL, {
        type: 'batchHealthData',
        data: batchData
      });
    }
  }
}

2. 冲突解决策略

// HealthDataConflictResolver.ets
class HealthDataConflictResolver {
  async resolveConflict(localData: DailyHealthData, remoteData: DailyHealthData): Promise<DailyHealthData> {
    // 简单策略:取各指标的最大值
    return {
      steps: Math.max(localData.steps, remoteData.steps),
      distance: Math.max(localData.distance, remoteData.distance),
      calories: Math.max(localData.calories, remoteData.calories),
      heartRate: this.resolveHeartRate(localData.heartRate, remoteData.heartRate),
      timestamp: Date.now()
    };
  }
  
  private resolveHeartRate(local?: number, remote?: number): number | undefined {
    if (local === undefined) return remote;
    if (remote === undefined) return local;
    
    // 取最近的数据
    return local; // 实际实现可能需要更复杂的逻辑
  }
}

六、项目扩展方向

  1. ​多健康指标支持​​:增加心率、睡眠等数据监测
  2. ​目标设置与提醒​​:设置每日步数目标并提醒
  3. ​社交功能​​:与好友分享健康数据
  4. ​健康建议​​:基于数据分析提供个性化建议
  5. ​多设备协同​​:智能分配数据采集任务
// 扩展示例:多指标支持
interface ExtendedHealthData extends DailyHealthData {
  sleepHours?: number;
  bloodPressure?: {
    systolic: number;
    diastolic: number;
  };
  bloodOxygen?: number;
}

// 扩展示例:目标提醒
class GoalReminder {
  private dailyGoal: number = 10000;
  
  checkGoal(steps: number) {
    if (steps >= this.dailyGoal) {
      notification.publish({
        content: {
          title: '目标达成',
          text: `恭喜您完成了今日${this.dailyGoal}步的目标!`
        }
      });
    } else if (steps >= this.dailyGoal * 0.8) {
      notification.publish({
        content: {
          title: '目标接近',
          text: `您已完成${steps}步,距离目标还有${this.dailyGoal - steps}步`
        }
      });
    }
  }
}

通过以上实现,我们创建了一个功能完善的跨设备健康助手应用,能够实时监测步数、同步健康数据并提供可视化分析。系统充分利用了HarmonyOS的分布式能力,确保用户在不同设备上获得一致的健康数据体验。

Logo

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

更多推荐