This content originally appeared on DEV Community and was authored by linzhongxue
Development Guide for Smart Sports Applications Based on HarmonyOS Next
Building Your First Fitness App from Scratch
In recent years, fitness and health applications have become increasingly popular. As a developer, you may want to create an app that tracks users’ workout data. Today, I’ll guide you through building a fully functional fitness app using HarmonyOS Next and AppGallery Connect.
Preparing the Development Environment
First, we need to set up our development tools. Open DevEco Studio and create a new project:
- Select the “Application” template
- Choose “Phone” as the device type
- Select ArkTS as the language
- Name the project “SportsTracker”
After creating the project, we’ll configure the AppGallery Connect services. In the AGC console, create a new project and enable the following services:
- Authentication Service (for user login)
- Cloud Database (to store workout data)
- Cloud Storage (to save user-uploaded workout photos)
Download the agconnect-services.json
file and place it in the entry/src/main/resources
directory.
Building the Basic Interface
Let’s start by creating a simple main interface. In the entry/src/main/ets/pages
directory, create a new Index.ets
file:
@Entry
@Component
struct Index {
@State currentTab: string = 'home'
build() {
Column() {
// Top title bar
Row() {
Image($r('app.media.logo'))
.width(40)
.height(40)
.margin(10)
Text('Fitness Tracker')
.fontSize(20)
.fontWeight(FontWeight.Bold)
}
.width('100%')
.justifyContent(FlexAlign.Start)
.backgroundColor('#f5f5f5')
// Content area
TabContent(this.currentTab)
// Bottom navigation bar
Tabs({ barPosition: BarPosition.End }) {
TabContent() {
Text('Home Content')
}.tabBar('Home')
TabContent() {
Text('Workout Records')
}.tabBar('Records')
TabContent() {
Text('Profile')
}.tabBar('Me')
}
.barWidth('100%')
.barHeight(60)
}
.width('100%')
.height('100%')
}
}
@Component
struct TabContent {
@Link currentTab: string
build() {
if (this.currentTab === 'home') {
HomePage()
} else if (this.currentTab === 'records') {
RecordsPage()
} else {
ProfilePage()
}
}
}
This basic interface includes a top title bar, content area, and bottom navigation bar. We’ve used the Tabs
component to implement page switching.
Implementing Workout Data Collection
The core functionality of a fitness app is collecting workout data. HarmonyOS provides rich sensor APIs that make it easy to gather various types of workout data.
Create a new sensor
directory under entry/src/main/ets
, then create a SportsSensor.ts
file:
import sensor from '@ohos.sensor';
// Workout sensor management class
export class SportsSensor {
private stepCounter: number = 0;
private heartRate: number = 0;
private calorie: number = 0;
// Initialize sensors
initSensors() {
try {
// Step counter sensor
sensor.on(sensor.SensorId.STEP_COUNTER, (data) => {
this.stepCounter = data.steps;
console.log(`Current steps: ${this.stepCounter}`);
});
// Heart rate sensor
sensor.on(sensor.SensorId.HEART_RATE, (data) => {
this.heartRate = data.heartRate;
console.log(`Current heart rate: ${this.heartRate}`);
});
// Accelerometer (for calculating calories)
sensor.on(sensor.SensorId.ACCELEROMETER, (data) => {
// Simple calorie calculation
const intensity = Math.sqrt(data.x*data.x + data.y*data.y + data.z*data.z);
this.calorie += intensity * 0.001;
console.log(`Calories burned: ${this.calorie.toFixed(2)}`);
});
} catch (error) {
console.error(`Sensor initialization failed: ${error}`);
}
}
// Get current steps
getSteps(): number {
return this.stepCounter;
}
// Get current heart rate
getHeartRate(): number {
return this.heartRate;
}
// Get calories burned
getCalorie(): number {
return this.calorie;
}
// Stop sensors
stopSensors() {
sensor.off(sensor.SensorId.STEP_COUNTER);
sensor.off(sensor.SensorId.HEART_RATE);
sensor.off(sensor.SensorId.ACCELEROMETER);
}
}
Implementing User Authentication
A user system is an essential component of any app. We can quickly implement user login functionality using AppGallery Connect’s authentication service.
Create a service
directory under entry/src/main/ets
, then create an AuthService.ts
file:
import agconnect from '@ohos/agconnect';
import { agc } from '@ohos/agconnect-auth';
export class AuthService {
// User login status
@State isLoggedIn: boolean = false;
// Current user information
@State currentUser: agc.User | null = null;
constructor() {
// Check if a user is already logged in
this.checkLoginStatus();
}
// Check login status
private checkLoginStatus() {
this.currentUser = agconnect.auth().getCurrentUser();
this.isLoggedIn = this.currentUser !== null;
}
// Email login
async loginWithEmail(email: string, password: string): Promise<boolean> {
try {
const user = await agconnect.auth().signInWithEmailAndPassword(email, password);
this.currentUser = user;
this.isLoggedIn = true;
return true;
} catch (error) {
console.error(`Login failed: ${error}`);
return false;
}
}
// Anonymous login
async anonymousLogin(): Promise<boolean> {
try {
const user = await agconnect.auth().signInAnonymously();
this.currentUser = user;
this.isLoggedIn = true;
return true;
} catch (error) {
console.error(`Anonymous login failed: ${error}`);
return false;
}
}
// Register new user
async register(email: string, password: string): Promise<boolean> {
try {
await agconnect.auth().createUserWithEmailAndPassword(email, password);
return true;
} catch (error) {
console.error(`Registration failed: ${error}`);
return false;
}
}
// Logout
async logout(): Promise<void> {
try {
await agconnect.auth().signOut();
this.currentUser = null;
this.isLoggedIn = false;
} catch (error) {
console.error(`Logout failed: ${error}`);
}
}
}
Data Storage and Synchronization
Workout data needs to be stored persistently. We can use AppGallery Connect’s cloud database service for this purpose.
Create a DataService.ts
file in the service
directory:
import agconnect from '@ohos/agconnect';
import { cloud } from '@ohos/agconnect-cloud';
export class DataService {
private db = cloud.database();
// Save workout record
async saveWorkoutRecord(record: WorkoutRecord): Promise<boolean> {
try {
const user = agconnect.auth().getCurrentUser();
if (!user) {
console.error('User not logged in');
return false;
}
await this.db.collection('workouts').add({
userId: user.uid,
date: new Date().toISOString(),
steps: record.steps,
heartRate: record.heartRate,
calorie: record.calorie,
duration: record.duration
});
return true;
} catch (error) {
console.error(`Failed to save record: ${error}`);
return false;
}
}
// Get user workout records
async getUserWorkouts(userId: string): Promise<WorkoutRecord[]> {
try {
const result = await this.db.collection('workouts')
.where({ userId: userId })
.orderBy('date', 'desc')
.get();
return result.data.map((doc: any) => ({
id: doc.id,
date: doc.date,
steps: doc.steps,
heartRate: doc.heartRate,
calorie: doc.calorie,
duration: doc.duration
}));
} catch (error) {
console.error(`Failed to get records: ${error}`);
return [];
}
}
}
interface WorkoutRecord {
id?: string;
date: string;
steps: number;
heartRate: number;
calorie: number;
duration: number;
}
Implementing Data Visualization
Data visualization helps users better understand their workout progress. Let’s implement a simple chart component.
Create a WorkoutChart.ets
file in the entry/src/main/ets/components
directory:
@Component
export struct WorkoutChart {
@Prop data: {date: string, value: number}[]
@Prop color: string = '#3366ff'
@Prop title: string = ''
build() {
Column() {
// Chart title
Text(this.title)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 10 })
// Chart container
Row() {
// Y-axis labels
Column() {
Text(Math.max(...this.data.map(d => d.value)).toString())
.fontSize(12)
Text('0')
.fontSize(12)
.margin({ top: '80%' })
}
.width(30)
// Chart body
Stack() {
// Background grid lines
ForEach(Array.from({length: 5}), (_, i) => {
Line()
.width('100%')
.height(1)
.backgroundColor('#eeeeee')
.margin({ top: `${i * 25}%` })
})
// Data line
Path()
.width('100%')
.height('100%')
.commands(this.getPathCommands())
.stroke(this.color)
.strokeWidth(2)
.fillOpacity(0)
}
.height(150)
.width('80%')
// X-axis date labels
Column() {
Text(this.data[0]?.date.split('T')[0] || '')
.fontSize(10)
.margin({ left: 10 })
Text(this.data[this.data.length - 1]?.date.split('T')[0] || '')
.fontSize(10)
.margin({ left: '80%' })
}
.width('100%')
}
.width('100%')
}
.padding(10)
}
// Generate path commands
private getPathCommands(): string {
if (this.data.length === 0) return '';
const maxValue = Math.max(...this.data.map(d => d.value));
const step = 100 / (this.data.length - 1);
let commands = `M0 ${100 - (this.data[0].value / maxValue) * 100}`;
for (let i = 1; i < this.data.length; i++) {
const x = i * step;
const y = 100 - (this.data[i].value / maxValue) * 100;
commands += ` L${x} ${y}`;
}
return commands;
}
}
Integrating All Features
Now, let’s integrate all the modules to complete the main page implementation.
Modify the HomePage
component in the Index.ets
file:
@Component
struct HomePage {
private sensorManager = new SportsSensor();
private authService = new AuthService();
private dataService = new DataService();
@State currentSteps: number = 0;
@State currentHeartRate: number = 0;
@State currentCalorie: number = 0;
@State isTracking: boolean = false;
@State workoutHistory: WorkoutRecord[] = [];
aboutToAppear() {
this.loadWorkoutHistory();
}
// Load workout history
private async loadWorkoutHistory() {
const user = this.authService.currentUser;
if (user) {
this.workoutHistory = await this.dataService.getUserWorkouts(user.uid);
}
}
// Start/stop workout tracking
toggleTracking() {
if (this.isTracking) {
this.sensorManager.stopSensors();
// Save current workout record
this.saveWorkoutRecord();
} else {
this.sensorManager.initSensors();
// Update data every second
setInterval(() => {
this.currentSteps = this.sensorManager.getSteps();
this.currentHeartRate = this.sensorManager.getHeartRate();
this.currentCalorie = this.sensorManager.getCalorie();
}, 1000);
}
this.isTracking = !this.isTracking;
}
// Save workout record
private async saveWorkoutRecord() {
const record: WorkoutRecord = {
date: new Date().toISOString(),
steps: this.currentSteps,
heartRate: Math.round(this.currentHeartRate),
calorie: Math.round(this.currentCalorie),
duration: 60 // Assuming 60 minutes of workout
};
const success = await this.dataService.saveWorkoutRecord(record);
if (success) {
console.log('Workout record saved successfully');
this.loadWorkoutHistory();
}
}
build() {
Column() {
// Workout data overview
Row() {
Column() {
Text('Steps')
.fontSize(14)
Text(this.currentSteps.toString())
.fontSize(24)
.fontWeight(FontWeight.Bold)
}
.margin(10)
Column() {
Text('Heart Rate')
.fontSize(14)
Text(this.currentHeartRate.toString())
.fontSize(24)
.fontWeight(FontWeight.Bold)
}
.margin(10)
Column() {
Text('Calories')
.fontSize(14)
Text(this.currentCalorie.toFixed(0))
.fontSize(24)
.fontWeight(FontWeight.Bold)
}
.margin(10)
}
.justifyContent(FlexAlign.SpaceAround)
.width('100%')
.margin({ top: 20 })
// Start/Stop button
Button(this.isTracking ? 'Stop Workout' : 'Start Workout')
.onClick(() => this.toggleTracking())
.width(200)
.margin(20)
// History chart
if (this.workoutHistory.length > 0) {
WorkoutChart({
data: this.workoutHistory.slice(0, 7).map(record => ({
date: record.date,
value: record.steps
})),
title: 'Last 7 Days Step Count',
color: '#4CAF50'
})
}
}
.width('100%')
.height('100%')
}
}
Testing and Publishing
After development, we need thorough testing:
- Test all features using the emulator in DevEco Studio
- Connect real devices to test sensor data collection
- Test data synchronization under different network conditions
- Verify the user login process is smooth
Once testing passes, we can prepare for publishing:
- Create an app in the AppGallery Connect console
- Configure app information, icons, and screenshots
- Build the release version
- Submit for review
Future Enhancement Directions
This basic version can be further improved:
- Add support for more workout types (running, cycling, etc.)
- Implement social features for sharing workout achievements
- Add an achievement system to motivate users
- Enhance data visualization with more chart types
- Support smart wearable devices for multi-device data sync
Conclusion
Through this article, we’ve completed the development of a fitness and health app based on HarmonyOS Next. From sensor data collection to user authentication, data storage, and visualization, we’ve covered the main aspects of building a complete application.
HarmonyOS Next and AppGallery Connect provide powerful infrastructure, allowing us to focus on implementing business logic rather than reinventing the wheel. I hope this example helps you quickly get started with HarmonyOS app development. I look forward to seeing more excellent apps from you!
This content originally appeared on DEV Community and was authored by linzhongxue