Comprehensive Guide to Developing a Sports Social App on HarmonyOS Next



This content originally appeared on DEV Community and was authored by linzhongxue

Comprehensive Guide to Developing a Sports Social App on HarmonyOS Next

Building Your First Sports Social App from Scratch

After meeting many like-minded friends at the gym, I realized how great it would be to have a dedicated social platform for fitness enthusiasts. Fortunately, HarmonyOS Next provides powerful development tools and ecosystem support. Today, let’s bring this idea to life together.

Preparation Before Development

First, open DevEco Studio and create a new project. I recommend selecting the “Empty Ability” template, choosing ArkTS as the language, and the Stage model for compatibility mode. This ensures we’re using the latest development paradigm.

// Application entry file EntryAbility.ts
import UIAbility from '@ohos.app.ability.UIAbility';
import window from '@ohos.window';

export default class EntryAbility extends UIAbility {
  // Initialize when the app starts
  onCreate() {
    console.log('Sports social app is starting up!');
  }

  // Create main window
  onWindowStageCreate(windowStage: window.WindowStage) {
    windowStage.loadContent('pages/HomePage', (err) => {
      if (err) {
        console.error('Error loading page:', err);
        return;
      }
      console.log('Homepage loaded successfully!');
    });
  }
}

Designing and Implementing the User System

Developing Login and Registration Features

The foundation of any sports social app is its user system. We can use AppGallery Connect’s authentication service to avoid building our own user system from scratch.

First, enable the authentication service in the AGC console, supporting phone number, email, and anonymous login. Then integrate the AGC SDK into the project:

// User service UserService.ts
import agconnect from '@hw-agconnect/api-ohos';
import '@hw-agconnect/auth-ohos';

class UserService {
  // Initialize AGC authentication
  static init() {
    agconnect.instance().init(this.context);
    console.log('User service initialized');
  }

  // Phone number login
  static async loginWithPhone(phone: string, code: string) {
    try {
      const credential = agconnect.auth.PhoneAuthProvider.credentialWithVerifyCode(
        phone,
        code
      );
      await agconnect.auth().signIn(credential);
      console.log('Phone login successful');
      return true;
    } catch (error) {
      console.error('Login error:', error);
      return false;
    }
  }

  // Get current user
  static getCurrentUser() {
    return agconnect.auth().currentUser;
  }
}

export default UserService;

Designing the Profile Page

With login functionality in place, let’s design the profile page to display user fitness data and basic information.

// Profile page ProfilePage.ets
@Component
struct UserAvatar {
  @Prop avatarUrl: string = 'common/default_avatar.png'

  build() {
    Image(this.avatarUrl)
      .width(80)
      .height(80)
      .borderRadius(40)
      .margin(10)
  }
}

@Entry
@Component
struct ProfilePage {
  @State userInfo: {
    nickname: string,
    level: number,
    totalDistance: number
  } = {
    nickname: 'Fitness Enthusiast',
    level: 3,
    totalDistance: 156.8
  }

  build() {
    Column() {
      // User avatar section
      UserAvatar({ avatarUrl: 'common/user_avatar.jpg' })

      // Basic info
      Text(this.userInfo.nickname)
        .fontSize(24)
        .fontWeight(FontWeight.Bold)

      // Fitness data display
      Row() {
        Column() {
          Text('Lv.' + this.userInfo.level)
            .fontSize(18)
          Text('Fitness Level')
            .fontSize(12)
            .fontColor('#666')
        }
        .margin({ right: 20 })

        Column() {
          Text(this.userInfo.totalDistance + 'km')
            .fontSize(18)
          Text('Total Distance')
            .fontSize(12)
            .fontColor('#666')
        }
      }
      .margin({ top: 15 })

      // Edit profile button
      Button('Edit Profile')
        .width('60%')
        .margin({ top: 30 })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }
}

Implementing Fitness Data Tracking

Integrating Health Data

HarmonyOS provides comprehensive health data interfaces that allow us to directly access user fitness metrics.

// Fitness service SportService.ts
import { health } from '@kit.HealthKit';

class SportService {
  // Request health data permissions
  static async requestHealthPermission() {
    try {
      const permissions = [
        'ohos.permission.health.READ_HEALTH_DATA',
        'ohos.permission.health.WRITE_HEALTH_DATA'
      ];
      await abilityAccessCtrl.requestPermissionsFromUser(permissions);
      console.log('Health data permissions granted');
    } catch (error) {
      console.error('Permission request error:', error);
    }
  }

  // Get today's step count
  static async getTodaySteps() {
    try {
      const now = new Date();
      const startOfDay = new Date(now.getFullYear(), now.getMonth(), now.getDate());

      const result = await health.getHealthData({
        startTime: startOfDay.getTime(),
        endTime: now.getTime(),
        dataType: 'STEP_COUNT'
      });

      return result.length > 0 ? result[0].value : 0;
    } catch (error) {
      console.error('Failed to get steps:', error);
      return 0;
    }
  }
}

export default SportService;

Fitness Tracking Interface

Let’s design an attractive interface to display fitness data:

// Fitness record page SportRecordPage.ets
@Component
struct StepCounter {
  @Prop currentSteps: number
  @Prop targetSteps: number = 10000

  build() {
    Column() {
      // Circular progress bar
      Stack() {
        Circle({ width: 180, height: 180 })
          .strokeWidth(10)
          .stroke('#eee')

        Circle({ width: 180, height: 180 })
          .strokeWidth(10)
          .stroke('#ff5a5f')
          .sweepAngle(this.currentSteps / this.targetSteps * 360)
      }

      // Step count display
      Text(this.currentSteps.toString())
        .fontSize(36)
        .margin({ top: 20 })

      Text('/ ' + this.targetSteps + ' steps')
        .fontSize(16)
        .fontColor('#666')
    }
    .width('100%')
    .margin({ top: 30 })
  }
}

@Entry
@Component
struct SportRecordPage {
  @State steps: number = 0

  aboutToAppear() {
    SportService.getTodaySteps().then(steps => {
      this.steps = steps;
    });
  }

  build() {
    Column() {
      StepCounter({ currentSteps: this.steps })

      // Other fitness metrics
      Row() {
        Column() {
          Text('5.6')
            .fontSize(24)
          Text('kilometers')
            .fontSize(14)
            .fontColor('#666')
        }
        .margin({ right: 30 })

        Column() {
          Text('420')
            .fontSize(24)
          Text('calories')
            .fontSize(14)
            .fontColor('#666')
        }
      }
      .margin({ top: 40 })

      // Start workout button
      Button('Start Workout Tracking')
        .type(ButtonType.Capsule)
        .width('80%')
        .height(50)
        .margin({ top: 50 })
    }
    .width('100%')
    .height('100%')
    .padding(20)
  }
}

Developing Social Features

Post Creation Functionality

The core of a sports social app is sharing capabilities. Let’s implement post creation:

// Post service PostService.ts
import { clouddb } from '@hw-agconnect/database-ohos';

class PostService {
  private static cloudDBZone: clouddb.CloudDBZone;

  // Initialize cloud database
  static async init() {
    const config = new clouddb.CloudDBZoneConfig(
      'SportSocialZone',
      clouddb.CloudDBZoneSyncProperty.CLOUDDBZONE_CLOUD_CACHE,
      clouddb.CloudDBZoneAccessProperty.CLOUDDBZONE_PUBLIC
    );

    this.cloudDBZone = await clouddb.CloudDBZone.open(config);
    await clouddb.CloudDBZone.registerObjectClass(this.cloudDBZone, 'Post');
    console.log('Post database initialized');
  }

  // Create new post
  static async createPost(content: string, images?: string[]) {
    const post = {
      id: generateId(),
      userId: UserService.getCurrentUser().uid,
      content,
      images: images || [],
      likes: 0,
      comments: 0,
      createTime: new Date().getTime()
    };

    try {
      await this.cloudDBZone.executeUpsert('Post', [post]);
      console.log('Post published successfully');
      return true;
    } catch (error) {
      console.error('Failed to publish post:', error);
      return false;
    }
  }
}

export default PostService;

Post Feed Display

// Post feed page PostListPage.ets
@Component
struct PostItem {
  @Prop post: {
    id: string,
    userId: string,
    content: string,
    likes: number,
    comments: number
  }

  @State isLiked: boolean = false

  build() {
    Column() {
      // User info bar
      Row() {
        UserAvatar({ avatarUrl: 'common/user_avatar.jpg' })

        Column() {
          Text('Fitness Pro')
            .fontSize(16)
          Text('2 hours ago')
            .fontSize(12)
            .fontColor('#999')
        }
        .margin({ left: 10 })
      }
      .width('100%')
      .justifyContent(FlexAlign.Start)

      // Post content
      Text(this.post.content)
        .fontSize(16)
        .margin({ top: 10, bottom: 10 })
        .width('100%')

      // Interaction area
      Row() {
        Image(this.isLiked ? 'common/liked.png' : 'common/like.png')
          .width(20)
          .height(20)
          .onClick(() => {
            this.isLiked = !this.isLiked;
          })
        Text(this.post.likes + (this.isLiked ? 1 : 0) + '')
          .fontSize(14)
          .margin({ left: 5 })

        Image('common/comment.png')
          .width(20)
          .height(20)
          .margin({ left: 20 })
        Text(this.post.comments + '')
          .fontSize(14)
          .margin({ left: 5 })
      }
      .width('100%')
      .margin({ top: 10 })
    }
    .padding(15)
    .borderRadius(10)
    .backgroundColor('#fff')
    .margin({ bottom: 10 })
    .width('100%')
  }
}

@Entry
@Component
struct PostListPage {
  @State posts: any[] = []

  aboutToAppear() {
    this.loadPosts();
  }

  async loadPosts() {
    // In practice, this should fetch data from cloud database
    this.posts = [
      {
        id: '1',
        userId: 'user1',
        content: 'Ran 5km this morning, feeling great!',
        likes: 12,
        comments: 3
      },
      {
        id: '2',
        userId: 'user2',
        content: 'Day 3 at the gym, keeping it up!',
        likes: 8,
        comments: 2
      }
    ];
  }

  build() {
    Column() {
      List({ space: 10 }) {
        ForEach(this.posts, (post) => {
          ListItem() {
            PostItem({ post: post })
          }
        })
      }
      .width('100%')
      .layoutWeight(1)
    }
    .width('100%')
    .height('100%')
    .padding(10)
    .backgroundColor('#f5f5f5')
  }
}

App Optimization and Release

Performance Optimization Tips

During actual development, I discovered several techniques to improve app fluidity:

  1. Lazy image loading: Use LazyForEach for images in posts to load them during scrolling
  2. Data caching: Cache user info and frequently used data locally to reduce network requests
  3. Component reuse: Encapsulate common components like user avatars separately
// Optimized image loading component
@Component
struct LazyImage {
  @Prop src: string
  @State loaded: boolean = false

  build() {
    Stack() {
      if (!this.loaded) {
        Progress()
          .width(50)
          .height(50)
      }

      Image(this.src)
        .onComplete(() => {
          this.loaded = true;
        })
        .opacity(this.loaded ? 1 : 0)
    }
    .width('100%')
    .aspectRatio(1)
  }
}

Preparing for App Store Submission

After development is complete, we need to:

  1. Complete app information in the AGC console
  2. Generate signing certificates
  3. Build release version
  4. Submit for review

Remember to configure all required permissions in config.json:

{
  "module": {
    "reqPermissions": [
      {
        "name": "ohos.permission.health.READ_HEALTH_DATA",
        "reason": "For tracking your fitness data"
      },
      {
        "name": "ohos.permission.INTERNET",
        "reason": "For connecting to network services"
      }
    ]
  }
}

Final Thoughts

Through this project, we’ve fully implemented the main features of a sports social app. The development experience with HarmonyOS Next is truly excellent, particularly the simplicity of ArkTS language and the convenience of AGC services, which greatly improve development efficiency.

There are many potential extensions for this app, such as:

  • Adding workout route tracking
  • Implementing a friends system
  • Developing fitness achievement systems
  • Enhancing workout data analysis

I hope this tutorial provides inspiration. If you have any questions, feel free to discuss them in the comments. Looking forward to seeing your sports social app creations!


This content originally appeared on DEV Community and was authored by linzhongxue