This content originally appeared on DEV Community and was authored by linzhongxue
Developing a Fitness & Health App on HarmonyOS Next: Heart Rate Monitoring and Health Reporting System
This article explores how to build a feature-rich fitness and health application using the HarmonyOS SDK and AppGallery Connect. We’ll focus on core functionalities: real-time heart rate monitoring, cloud data synchronization, health report generation, and an achievement incentive system.
1. Core Feature Design and Tech Stack
Core Modules:
- Heart Rate Monitoring: Capture real-time heart rate data using device sensors.
- Data Sync: Securely store heart rate data in AppGallery Connect Cloud DB.
- Health Reports: Analyze data using cloud functions to generate daily/weekly reports.
- Achievement System: Display leaderboards and badges based on activity data.
Tech Stack:
- Frontend: HarmonyOS ArkTS UI framework.
- Data Storage: AppGallery Connect Cloud DB (NoSQL).
- Backend Logic: AppGallery Connect Cloud Functions.
- Data Collection: HarmonyOS Sensor Framework.
2. Real-Time Heart Rate Monitoring (ArkTS)
1. Permission Request & Sensor Initialization
// pages/HeartRateMonitor.ets
import sensor from '@ohos.sensor';
import abilityAccessCtrl, { Permissions } from '@ohos.abilityAccessCtrl';
// 1. Dynamically request heart rate sensor permission
async function requestPermission(): Promise<void> {
try {
let atManager = abilityAccessCtrl.createAtManager();
let permission: Permissions = 'ohos.permission.HEALTH_DATA';
await atManager.requestPermissionsFromUser(getContext(this), [permission]);
console.info('Heart rate permission granted');
} catch (err) {
console.error(`Permission request failed: ${err.code}, ${err.message}`);
}
}
// 2. Initialize heart rate sensor
let heartRateSensor: sensor.HeartRateResponse | null = null;
function initHeartRateSensor(): void {
try {
// Get sensor instance
heartRateSensor = sensor.getSensor(sensor.SensorId.HEART_RATE);
if (!heartRateSensor) {
console.error('Device lacks heart rate sensor');
return;
}
// Set data callback
heartRateSensor.on('data', (data: sensor.HeartRateResponse) => {
if (data.heartRate) {
// Update UI (assumes @State heartRate: number)
this.heartRate = data.heartRate;
console.info(`Real-time heart rate: ${this.heartRate} bpm`);
// Sync data to cloud
this.uploadHeartRateData(this.heartRate);
}
});
// Error handling
heartRateSensor.on('error', (err: BusinessError) => {
console.error(`Sensor error: ${err.code}, ${err.message}`);
});
} catch (err) {
console.error(`Sensor init failed: ${err.code}, ${err.message}`);
}
}
// 3. Start/stop monitoring
function startMonitoring(): void {
if (heartRateSensor) {
try {
// Set sampling rate (NORMAL ≈ 5s intervals)
heartRateSensor.setInterval(sensor.Interval.NORMAL);
heartRateSensor.start();
} catch (err) {
console.error(`Start failed: ${err.code}, ${err.message}`);
}
}
}
function stopMonitoring(): void {
if (heartRateSensor) {
try {
heartRateSensor.stop();
} catch (err) {
console.error(`Stop failed: ${err.code}, ${err.message}`);
}
}
}
// Initialize on page display
aboutToAppear(): void {
requestPermission().then(() => {
initHeartRateSensor();
});
}
// Release sensor on page hide
aboutToDisappear(): void {
stopMonitoring();
}
Code Notes:
- Permissions: Uses
abilityAccessCtrl
to requestHEALTH_DATA
dynamically. - Sensor Initialization: Retrieves sensor via
sensor.getSensor()
. - Data Callback: Updates UI and triggers cloud upload when new data arrives.
- Lifecycle Management: Starts/stops monitoring with page visibility to optimize performance.
3. Syncing Data to AppGallery Connect Cloud DB
1. Cloud DB Setup
- Create a project in AppGallery Connect console and enable Cloud DB.
- Define object type
HeartRateData
with fields:-
userId: String
-
heartRate: Integer
-
timestamp: Date
-
- Configure storage location and access rules (user-level permissions recommended).
2. ArkTS Data Upload Code
// utils/CloudDBManager.ets
import cloud from '@ohos.cloud.agconnect';
import { HeartRateData } from '../model/HeartRateData';
// Initialize Cloud DB
const agcCloud = cloud.agconnectCloudDb();
const cloudDbZone = agcCloud.agconnectCloudDbZone();
// Data model class (matches cloud object type)
export class HeartRateData {
userId: string = '';
heartRate: number = 0;
timestamp: Date = new Date();
constructor(userId: string, heartRate: number, timestamp: Date) {
this.userId = userId;
this.heartRate = heartRate;
this.timestamp = timestamp;
}
static getObjectTypeName(): string {
return 'HeartRateData';
}
}
// Upload heart rate data
export async function uploadHeartRateData(heartRate: number): Promise<void> {
try {
// 1. Get current user ID (requires AGC Auth)
const currentUser = getCurrentUser();
if (!currentUser) {
console.warn('Upload skipped: User not logged in');
return;
}
// 2. Create data object
const hrData = new HeartRateData(currentUser.uid, heartRate, new Date());
// 3. Upsert operation
const upsertResult = await cloudDbZone.upsert(HeartRateData.getObjectTypeName(), [hrData]);
console.info('Data uploaded!', upsertResult);
} catch (err) {
console.error(`Upload failed: ${err.code}, ${err.message}`);
// Add retry/local caching logic here
}
}
Code Notes:
- Model Definition: Local class mirrors cloud object structure.
- Cloud DB Initialization: Prepares the database zone for operations.
- Data Operation:
upsert
inserts or updates records in Cloud DB. - User Binding: Data linked to user ID via AppGallery Connect Auth.
- Error Handling: Implements basic error logging (add retry logic for production).
4. Generating Health Reports (AppGallery Connect Cloud Function)
1. Cloud Function Logic (Node.js)
- Scenario: Calculate daily resting/average/min/max heart rate at midnight.
// functions/generateDailyReport.js
const agconnect = require('@agconnect/common-server');
const cloudDb = require('@agconnect/database-server').cloudDb;
exports.main = async function (event) {
agconnect.instance().init();
const agcCloudDb = cloudDb.cloudDb();
const cloudDbZone = agcCloudDb.agconnectCloudDbZone();
const calculateStats = async (userId) => {
// Get yesterday's data (00:00 - 23:59)
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
const startTime = new Date(yesterday.setHours(0, 0, 0, 0));
const endTime = new Date(yesterday.setHours(23, 59, 59, 999));
const query = cloudDb.CloudDBZoneQuery.where(HeartRateData)
.equalTo('userId', userId)
.greaterThanOrEqualTo('timestamp', startTime)
.lessThanOrEqualTo('timestamp', endTime);
const snapshot = await cloudDbZone.executeQuery(query);
const hrDataList = snapshot.getSnapshotObjects();
if (hrDataList.length === 0) return null;
const values = hrDataList.map(data => data.heartRate);
return {
date: startTime.toISOString().split('T')[0], // YYYY-MM-DD
min: Math.min(...values),
max: Math.max(...values),
avg: Math.round(values.reduce((a, b) => a + b, 0) / values.length),
resting: calculateRestingHr(values), // Custom calculation
userId: userId
};
};
const saveReport = async (report) => {
if (!report) return;
const reportObj = new HealthReport(report);
await cloudDbZone.upsert('HealthReport', [reportObj]);
console.log(`Report generated for ${report.userId} (${report.date})`);
};
// Target users (fetch dynamically in production)
const targetUsers = ['user123', 'user456'];
for (const userId of targetUsers) {
const report = await calculateStats(userId);
await saveReport(report);
}
return { message: 'Daily report task completed' };
};
// HealthReport model (define in Cloud DB)
class HealthReport {
constructor({ date, min, max, avg, resting, userId }) {
this.reportDate = date;
this.minHeartRate = min;
this.maxHeartRate = max;
this.avgHeartRate = avg;
this.restingHeartRate = resting;
this.userId = userId;
this.generatedAt = new Date();
}
}
Logic Breakdown:
- Scheduled Trigger: Configured to run daily via
timer
trigger. - Data Query: Fetches a user’s heart rate data for the previous day.
- Statistical Analysis: Computes min, max, average, and resting heart rate.
- Report Storage: Saves results to the
HealthReport
object type for client access.
5. Achievement System & Leaderboards (ArkTS + Cloud DB Queries)
1. Querying User Activity Data (Steps Example)
// pages/Achievements.ets
import { HealthReport } from '../model';
import { cloudDbZone } from '../utils/CloudDBManager';
@State dailyStats: HealthReport | null = null;
@State stepCount: number = 0; // From step sensor
// Fetch today's health report
async function fetchTodayReport(): Promise<void> {
try {
const currentUser = getCurrentUser();
if (!currentUser) return;
const today = new Date().toISOString().split('T')[0];
const query = cloudDb.CloudDBZoneQuery.where(HealthReport)
.equalTo('userId', currentUser.uid)
.equalTo('reportDate', today);
const snapshot = await cloudDbZone.executeQuery(query);
const reports = snapshot.getSnapshotObjects();
if (reports.length > 0) this.dailyStats = reports[0];
} catch (err) {
console.error('Fetch report failed:', err);
}
}
// Badge logic (e.g., 7 consecutive days达标)
checkConsistencyMedal(): void {
if (this.dailyStats && this.dailyStats.restingHeartRate < 60) {
this.consecutiveDays++;
if (this.consecutiveDays >= 7) {
console.log('Earned "Heart Steady Star" badge!');
}
} else {
this.consecutiveDays = 0;
}
}
// Step leaderboard (Top 10)
@State leaderboard: { userId: string, steps: number }[] = [];
async function fetchStepLeaderboard(): Promise<void> {
try {
const query = cloudDb.CloudDBZoneQuery.where('DailyStepSummary')
.equalTo('date', new Date().toISOString().split('T')[0])
.orderByDesc('totalSteps')
.limit(10);
const snapshot = await cloudDbZone.executeQuery(query);
this.leaderboard = snapshot.getSnapshotObjects();
} catch (err) {
console.error('Fetch leaderboard failed:', err);
}
}
UI Rendering Snippet:
build() {
Column() {
// Display health report
if (this.dailyStats) {
Card() {
Text(`Today's Report (${this.dailyStats.reportDate})`)
.fontSize(18)
Row() {
Text(`Avg: ${this.dailyStats.avgHeartRate}`)
Text(`Min: ${this.dailyStats.minHeartRate}`)
Text(`Max: ${this.dailyStats.maxHeartRate}`)
Text(`Resting: ${this.dailyStats.restingHeartRate}`)
}.justifyContent(FlexAlign.SpaceAround)
}
}
// Badge display
if (this.hasMedal) {
Image($r('app.medal.heart_steady'))
.width(80)
.height(80)
}
// Leaderboard
List() {
ForEach(this.leaderboard, (item, index) => {
ListItem() {
Row() {
Text(`${index + 1}. User_${item.userId.slice(0, 4)}`)
Text(`Steps: ${item.steps}`).fontColor(Color.Blue)
}
}
})
}
}
}
Feature Notes:
- Report Display: Pulls and shows daily reports from Cloud DB.
- Badge System: Awards badges based on custom rules (e.g., 7-day resting HR consistency).
- Leaderboards: Uses Cloud DB’s sorting (
orderByDesc
) and limits (limit
) for efficient top-10 queries.
6. Conclusion & Best Practices
This case study implements core features for a HarmonyOS Next fitness app:
- Accurate Data Capture: Leverages HarmonyOS sensors for reliable health data.
- Cloud Synchronization: Uses AppGallery Connect Cloud DB for persistent storage.
- Smart Analytics: Processes data via cloud functions for complex reporting.
- Engagement Features: Motivates users through badges and leaderboards.
Best Practices:
- Minimal Permissions: Request only essential health data permissions with clear explanations.
- Data Resilience: Implement local caching and retry mechanisms for network issues.
- Battery Optimization: Manage sensor lifecycle carefully.
- Privacy Protection: Encrypt sensitive health data (enabled by AppGallery Connect).
- Cloud Function Tuning: Paginate large operations and configure appropriate timeouts/memory.
- User-Centric UI: Present data clearly using visualizations like charts.
This framework supports extending to advanced features like sleep monitoring, activity recognition, and personalized insights, empowering developers to create compelling health applications.
This content originally appeared on DEV Community and was authored by linzhongxue

linzhongxue | Sciencx (2025-06-11T03:38:03+00:00) Developing a Fitness & Health App on HarmonyOS Next: Heart Rate Monitoring and Health Reporting System. Retrieved from https://www.scien.cx/2025/06/11/developing-a-fitness-health-app-on-harmonyos-next-heart-rate-monitoring-and-health-reporting-system/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.