Development Guide for Smart Sports Applications Based on HarmonyOS Next



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:

  1. Select the “Application” template
  2. Choose “Phone” as the device type
  3. Select ArkTS as the language
  4. 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:

  1. Test all features using the emulator in DevEco Studio
  2. Connect real devices to test sensor data collection
  3. Test data synchronization under different network conditions
  4. Verify the user login process is smooth

Once testing passes, we can prepare for publishing:

  1. Create an app in the AppGallery Connect console
  2. Configure app information, icons, and screenshots
  3. Build the release version
  4. Submit for review

Future Enhancement Directions

This basic version can be further improved:

  1. Add support for more workout types (running, cycling, etc.)
  2. Implement social features for sharing workout achievements
  3. Add an achievement system to motivate users
  4. Enhance data visualization with more chart types
  5. 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