DEV Community

linzhongxue
linzhongxue

Posted on

Building an Intelligent Learning Platform Using AppGallery Connect

Practical Development of Educational Applications Based on HarmonyOS Next: Building an Intelligent Learning Platform Using AppGallery Connect

1. Overview of AppGallery Connect and Educational Application Development

In today's digital education era, mobile applications have become essential learning tools. HarmonyOS Next, as Huawei's next-generation operating system, provides robust technical support for educational application development. AppGallery Connect, Huawei's developer service platform, integrates multiple service capabilities to help developers quickly build high-quality educational applications.

Educational applications typically require the following core features:

  • User account management and learning progress synchronization
  • Course content management and updates
  • Learning data statistics and analysis
  • Interactive features such as Q&A and quizzes
  • Multi-device collaborative learning experience

This tutorial will guide developers in using the ArkTS language and AppGallery Connect services to build a complete intelligent learning platform application. We will start with the basic architecture and gradually implement core functional modules.

2. Development Environment Setup and Project Initialization

Before coding, we need to prepare the development environment:

  1. Install the latest version of DevEco Studio (recommended version 4.0 or higher).
  2. Register a Huawei Developer account and complete real-name verification.
  3. Create a new project in AppGallery Connect and enable the required services.

Project Initialization Code

// Project entry file: entry/src/main/ets/entryability/EntryAbility.ts  
import Ability from '@ohos.app.ability.UIAbility';  
import window from '@ohos.window';  

export default class EntryAbility extends Ability {  
  onCreate(want, launchParam) {  
    console.info('EntryAbility onCreate');  
    // Initialize AppGallery Connect services  
    this.initAGC();  
  }  

  private async initAGC() {  
    try {  
      // Import AGC core module  
      const agconnect = await import('@hw-agconnect/core-ohos');  
      // Initialize AGC with project configuration  
      agconnect.default.instance().init(this.context);  
      console.info('AGC initialization successful');  
    } catch (error) {  
      console.error('AGC initialization failed:', error);  
    }  
  }  

  onWindowStageCreate(windowStage: window.WindowStage) {  
    // Set the main page  
    windowStage.loadContent('pages/Index', (err) => {  
      if (err) {  
        console.error('Failed to load page:', err);  
      }  
    });  
  }  
}  
Enter fullscreen mode Exit fullscreen mode

3. User Authentication and Learning Data Synchronization

Educational applications typically require a user system to save learning progress and personal data. AppGallery Connect provides comprehensive authentication services and cloud database functionality.

1. Implementation of User Authentication Module

// src/main/ets/model/UserModel.ts  
import { agconnect } from '@hw-agconnect/core-ohos';  
import { AGCAuth, AGConnectUser } from '@hw-agconnect/auth-ohos';  

export class UserModel {  
  // Get the current user  
  static getCurrentUser(): Promise<AGConnectUser | null> {  
    return AGCAuth.getInstance().getCurrentUser();  
  }  

  // Anonymous login  
  static async anonymousLogin(): Promise<AGConnectUser> {  
    try {  
      const user = await AGCAuth.getInstance().signInAnonymously();  
      console.info('Anonymous login successful:', user.getUid());  
      return user;  
    } catch (error) {  
      console.error('Anonymous login failed:', error);  
      throw error;  
    }  
  }  

  // Email registration  
  static async registerWithEmail(email: string, password: string): Promise<AGConnectUser> {  
    try {  
      const user = await AGCAuth.getInstance().createEmailUser(email, password);  
      console.info('Email registration successful:', user.getUid());  
      return user;  
    } catch (error) {  
      console.error('Email registration failed:', error);  
      throw error;  
    }  
  }  

  // Email login  
  static async loginWithEmail(email: string, password: string): Promise<AGConnectUser> {  
    try {  
      const user = await AGCAuth.getInstance().signInWithEmailAndPassword(email, password);  
      console.info('Email login successful:', user.getUid());  
      return user;  
    } catch (error) {  
      console.error('Email login failed:', error);  
      throw error;  
    }  
  }  
}  
Enter fullscreen mode Exit fullscreen mode

2. Learning Progress Synchronization Function

// src/main/ets/model/ProgressModel.ts  
import { agconnect } from '@hw-agconnect/core-ohos';  
import { AGCCloudDB, CloudDBZone, CloudDBZoneConfig, CloudDBZoneQuery } from '@hw-agconnect/clouddb-ohos';  
import { UserModel } from './UserModel';  

// Define learning progress data structure  
interface LearningProgress {  
  id: string; // Document ID  
  userId: string; // User ID  
  courseId: string; // Course ID  
  progress: number; // Learning progress (0-100)  
  lastUpdate: number; // Last update timestamp  
  notes?: string; // Study notes  
}  

export class ProgressModel {  
  private cloudDBZone: CloudDBZone | null = null;  

  // Initialize cloud database  
  async initCloudDB(): Promise<void> {  
    try {  
      const agcCloudDB = AGCCloudDB.getInstance();  
      const cloudDBZoneConfig = new CloudDBZoneConfig('LearningDB', CloudDBZoneConfig.CloudDBZoneSyncProperty.CLOUDDBZONE_CLOUD_CACHE);  
      this.cloudDBZone = await agcCloudDB.openCloudDBZone(cloudDBZoneConfig);  
      console.info('Cloud database initialization successful');  
    } catch (error) {  
      console.error('Cloud database initialization failed:', error);  
      throw error;  
    }  
  }  

  // Save learning progress  
  async saveProgress(courseId: string, progress: number, notes?: string): Promise<void> {  
    if (!this.cloudDBZone) {  
      await this.initCloudDB();  
    }  

    const user = await UserModel.getCurrentUser();  
    if (!user) {  
      throw new Error('User not logged in');  
    }  

    const progressData: LearningProgress = {  
      id: `${user.getUid()}_${courseId}`,  
      userId: user.getUid(),  
      courseId,  
      progress,  
      lastUpdate: new Date().getTime(),  
      notes  
    };  

    try {  
      await this.cloudDBZone!.executeUpsert(progressData);  
      console.info('Learning progress saved successfully');  
    } catch (error) {  
      console.error('Failed to save learning progress:', error);  
      throw error;  
    }  
  }  

  // Get learning progress  
  async getProgress(courseId: string): Promise<LearningProgress | null> {  
    if (!this.cloudDBZone) {  
      await this.initCloudDB();  
    }  

    const user = await UserModel.getCurrentUser();  
    if (!user) {  
      throw new Error('User not logged in');  
    }  

    try {  
      const query = CloudDBZoneQuery.where(LearningProgress).equalTo('id', `${user.getUid()}_${courseId}`);  
      const result = await this.cloudDBZone!.executeQuery(query);  
      return result.length > 0 ? result[0] : null;  
    } catch (error) {  
      console.error('Failed to get learning progress:', error);  
      throw error;  
    }  
  }  
}  
Enter fullscreen mode Exit fullscreen mode

4. Course Content Management Module

The core of educational applications is the management and display of course content. We can use AppGallery Connect's cloud storage and cloud function services to implement dynamic course updates.

1. Course Data Structure Definition

// src/main/ets/model/CourseModel.ts  
import { agconnect } from '@hw-agconnect/core-ohos';  
import { AGCCloudDB, CloudDBZone, CloudDBZoneConfig, CloudDBZoneQuery } from '@hw-agconnect/clouddb-ohos';  
import { AGCCloudStorage, UploadTask, DownloadTask } from '@hw-agconnect/storage-ohos';  

// Course data structure  
interface Course {  
  id: string; // Course ID  
  title: string; // Course title  
  description: string; // Course description  
  coverUrl: string; // Cover image URL  
  category: string; // Course category  
  duration: number; // Course duration (minutes)  
  difficulty: number; // Difficulty level (1-5)  
  createTime: number; // Creation timestamp  
  updateTime: number; // Update timestamp  
  isFree: boolean; // Whether it is free  
}  

// Course chapter structure  
interface Chapter {  
  id: string; // Chapter ID  
  courseId: string; // Associated course ID  
  title: string; // Chapter title  
  order: number; // Chapter order  
  videoUrl?: string; // Video URL  
  content?: string; // Chapter content (HTML format)  
  duration: number; // Chapter duration (minutes)  
}  

export class CourseModel {  
  private cloudDBZone: CloudDBZone | null = null;  
  private storage = AGCCloudStorage.getInstance();  

  // Initialize cloud database  
  async initCloudDB(): Promise<void> {  
    if (this.cloudDBZone) return;  

    try {  
      const agcCloudDB = AGCCloudDB.getInstance();  
      const cloudDBZoneConfig = new CloudDBZoneConfig('LearningDB', CloudDBZoneConfig.CloudDBZoneSyncProperty.CLOUDDBZONE_CLOUD_CACHE);  
      this.cloudDBZone = await agcCloudDB.openCloudDBZone(cloudDBZoneConfig);  
      console.info('Cloud database initialization successful');  
    } catch (error) {  
      console.error('Cloud database initialization failed:', error);  
      throw error;  
    }  
  }  

  // Get course list  
  async getCourseList(category?: string): Promise<Course[]> {  
    if (!this.cloudDBZone) {  
      await this.initCloudDB();  
    }  

    try {  
      let query = CloudDBZoneQuery.where(Course);  
      if (category) {  
        query = query.equalTo('category', category);  
      }  
      query = query.orderByDesc('updateTime');  
      return await this.cloudDBZone!.executeQuery(query);  
    } catch (error) {  
      console.error('Failed to get course list:', error);  
      throw error;  
    }  
  }  

  // Get course details  
  async getCourseDetail(courseId: string): Promise<{course: Course, chapters: Chapter[]}> {  
    if (!this.cloudDBZone) {  
      await this.initCloudDB();  
    }  

    try {  
      // Query course information  
      const courseQuery = CloudDBZoneQuery.where(Course).equalTo('id', courseId);  
      const courseResult = await this.cloudDBZone!.executeQuery(courseQuery);  
      if (courseResult.length === 0) {  
        throw new Error('Course does not exist');  
      }  

      // Query chapter information  
      const chapterQuery = CloudDBZoneQuery.where(Chapter)  
        .equalTo('courseId', courseId)  
        .orderByAsc('order');  
      const chapters = await this.cloudDBZone!.executeQuery(chapterQuery);  

      return {  
        course: courseResult[0],  
        chapters  
      };  
    } catch (error) {  
      console.error('Failed to get course details:', error);  
      throw error;  
    }  
  }  

  // Upload course cover image  
  async uploadCourseCover(fileUri: string): Promise<string> {  
    try {  
      // Generate unique filename  
      const timestamp = new Date().getTime();  
      const fileName = `course_covers/${timestamp}.jpg`;  

      // Create upload task  
      const uploadTask: UploadTask = this.storage.uploadFile({  
        path: fileName,  
        fileUri: fileUri  
      });  

      // Wait for upload to complete  
      await uploadTask;  

      // Get download URL  
      const downloadUrl = await this.storage.getDownloadUrl({ path: fileName });  
      return downloadUrl;  
    } catch (error) {  
      console.error('Failed to upload course cover:', error);  
      throw error;  
    }  
  }  
}  
Enter fullscreen mode Exit fullscreen mode

2. Course Display UI Implementation

// src/main/ets/pages/CourseListPage.ets  
@Component  
struct CourseListPage {  
  @State courseList: Course[] = [];  
  @State isLoading: boolean = true;  
  @State selectedCategory: string = 'all';  

  private courseModel = new CourseModel();  

  aboutToAppear() {  
    this.loadCourses();  
  }  

  private async loadCourses() {  
    this.isLoading = true;  
    try {  
      const category = this.selectedCategory === 'all' ? undefined : this.selectedCategory;  
      this.courseList = await this.courseModel.getCourseList(category);  
    } catch (error) {  
      console.error('Failed to load courses:', error);  
      // Add error prompt UI here  
    } finally {  
      this.isLoading = false;  
    }  
  }  

  build() {  
    Column() {  
      // Category filter  
      Segmented({ barPosition: BarPosition.Start }) {  
        ForEach(['all', 'programming', 'language', 'math', 'science'], (item: string) => {  
          Segment(item.toUpperCase())  
            .fontSize(14)  
            .fontWeight(FontWeight.Medium)  
        })  
      }  
      .margin(10)  
      .onChange((index: number) => {  
        this.selectedCategory = ['all', 'programming', 'language', 'math', 'science'][index];  
        this.loadCourses();  
      })  

      // Course list  
      if (this.isLoading) {  
        LoadingProgress()  
          .width(50)  
          .height(50)  
      } else {  
        Grid() {  
          ForEach(this.courseList, (course: Course) => {  
            GridItem() {  
              CourseCard({ course: course })  
            }  
          }, (course: Course) => course.id)  
        }  
        .columnsTemplate('1fr 1fr')  
        .columnsGap(10)  
        .rowsGap(10)  
        .padding(10)  
      }  
    }  
    .width('100%')  
    .height('100%')  
    .backgroundColor('#F5F5F5')  
  }  
}  

@Component  
struct CourseCard {  
  private course: Course;  

  build() {  
    Column() {  
      // Course cover  
      Image(this.course.coverUrl)  
        .width('100%')  
        .aspectRatio(1.5)  
        .objectFit(ImageFit.Cover)  
        .borderRadius(8)  

      // Course title  
      Text(this.course.title)  
        .fontSize(16)  
        .fontWeight(FontWeight.Medium)  
        .margin({ top: 8, bottom: 4 })  
        .maxLines(1)  
        .textOverflow({ overflow: TextOverflow.Ellipsis })  

      // Course description  
      Text(this.course.description)  
        .fontSize(12)  
        .maxLines(2)  
        .textOverflow({ overflow: TextOverflow.Ellipsis })  
        .margin({ bottom: 8 })  

      // Difficulty and duration  
      Row() {  
        // Difficulty stars  
        ForEach(Array.from({ length: this.course.difficulty }), (_, index) => {  
          Image($r('app.media.star_filled'))  
            .width(12)  
            .height(12)  
            .margin({ right: 2 })  
        })  

        // Duration  
        Text(`${this.course.duration} minutes`)  
          .fontSize(12)  
          .margin({ left: 8 })  
      }  
    }  
    .padding(10)  
    .backgroundColor(Color.White)  
    .borderRadius(8)  
    .shadow({ radius: 4, color: '#00000020', offsetX: 0, offsetY: 2 })  
  }  
}  
Enter fullscreen mode Exit fullscreen mode

5. Learning Interaction and Quiz Functionality

Interactivity is crucial for learning effectiveness in educational applications. Below, we implement a quiz module that includes question management and answering functionality.

1. Quiz Data Structure and Model

// src/main/ets/model/QuizModel.ts  
import { agconnect } from '@hw-agconnect/core-ohos';  
import { AGCCloudDB, CloudDBZone, CloudDBZoneConfig, CloudDBZoneQuery } from '@hw-agconnect/clouddb-ohos';  
import { UserModel } from './UserModel';  

// Quiz question structure  
interface QuizQuestion {  
  id: string; // Question ID  
  courseId: string; // Associated course ID  
  chapterId?: string; // Associated chapter ID (optional)  
  questionType: 'single' | 'multiple' | 'true_false' | 'fill_blank'; // Question type  
  questionText: string; // Question text  
  options?: string[]; // Options (for multiple-choice questions)  
  correctAnswers: string[]; // Correct answers  
  explanation?: string; // Answer explanation  
  difficulty: number; // Difficulty level (1-5)  
  createTime: number; // Creation timestamp  
}  

// User answer record  
interface UserQuizRecord {  
  id: string; // Record ID  
  userId: string; // User ID  
  questionId: string; // Question ID  
  userAnswers: string[]; // User answers  
  isCorrect: boolean; // Whether correct  
  answerTime: number; // Answer timestamp  
}  

export class QuizModel {  
  private cloudDBZone: CloudDBZone | null = null;  

  async initCloudDB(): Promise<void> {  
    if (this.cloudDBZone) return;  

    try {  
      const agcCloudDB = AGCCloudDB.getInstance();  
      const cloudDBZoneConfig = new CloudDBZoneConfig('LearningDB', CloudDBZoneConfig.CloudDBZoneSyncProperty.CLOUDDBZONE_CLOUD_CACHE);  
      this.cloudDBZone = await agcCloudDB.openCloudDBZone(cloudDBZoneConfig);  
      console.info('Cloud database initialization successful');  
    } catch (error) {  
      console.error('Cloud database initialization failed:', error);  
      throw error;  
    }  
  }  

  // Get course quiz questions  
  async getQuizQuestions(courseId: string, chapterId?: string): Promise<QuizQuestion[]> {  
    if (!this.cloudDBZone) {  
      await this.initCloudDB();  
    }  

    try {  
      let query = CloudDBZoneQuery.where(QuizQuestion)  
        .equalTo('courseId', courseId)  
        .orderByAsc('createTime');  

      if (chapterId) {  
        query = query.equalTo('chapterId', chapterId);  
      }  

      return await this.cloudDBZone!.executeQuery(query);  
    } catch (error) {  
      console.error('Failed to get quiz questions:', error);  
      throw error;  
    }  
  }  

  // Submit user answers  
  async submitAnswer(questionId: string, userAnswers: string[]): Promise<boolean> {  
    if (!this.cloudDBZone) {  
      await this.initCloudDB();  
    }  

    const user = await UserModel.getCurrentUser();  
    if (!user) {  
      throw new Error('User not logged in');  
    }  

    try {  
      // First, get question information  
      const questionQuery = CloudDBZoneQuery.where(QuizQuestion).equalTo('id', questionId);  
      const questions = await this.cloudDBZone!.executeQuery(questionQuery);  
      if (questions.length === 0) {  
        throw new Error('Question does not exist');  
      }  

      const question = questions[0];  
      // Determine if the answer is correct  
      const isCorrect = JSON.stringify(userAnswers.sort()) === JSON.stringify(question.correctAnswers.sort());  

      // Save answer record  
      const record: UserQuizRecord = {  
        id: `${user.getUid()}_${questionId}_${new Date().getTime()}`,  
        userId: user.getUid(),  
        questionId,  
        userAnswers,  
        isCorrect,  
        answerTime: new Date().getTime()  
      };  

      await this.cloudDBZone!.executeUpsert(record);  
      console.info('Answer record saved successfully');  

      return isCorrect;  
    } catch (error) {  
      console.error('Failed to submit answer:', error);  
      throw error;  
    }  
  }  

  // Get user quiz statistics  
  async getUserQuizStats(courseId: string): Promise<{total: number, correct: number}> {  
    if (!this.cloudDBZone) {  
      await this.initCloudDB();  
    }  

    const user = await UserModel.getCurrentUser();  
    if (!user) {  
      throw new Error('User not logged in');  
    }  

    try {  
      // Get all question IDs for the course  
      const questionQuery = CloudDBZoneQuery.where(QuizQuestion).equalTo('courseId', courseId);  
      const questions = await this.cloudDBZone!.executeQuery(questionQuery);  
      const questionIds = questions.map(q => q.id);  

      if (questionIds.length === 0) {  
        return { total: 0, correct: 0 };  
      }  

      // Query user answer records  
      const recordQuery = CloudDBZoneQuery.where(UserQuizRecord)  
        .equalTo('userId', user.getUid())  
        .in('questionId', questionIds);  

      const records = await this.cloudDBZone!.executeQuery(recordQuery);  

      // Calculate accuracy  
      const correctCount = records.filter(r => r.isCorrect).length;  

      return {  
        total: records.length,  
        correct: correctCount  
      };  
    } catch (error) {  
      console.error('Failed to get quiz statistics:', error);  
      throw error;  
    }  
  }  
}  
Enter fullscreen mode Exit fullscreen mode

2. Quiz UI Implementation

// src/main/ets/pages/QuizPage.ets  
@Component  
struct QuizPage {  
  @State questions: QuizQuestion[] = [];  
  @State currentIndex: number = 0;  
  @State selectedAnswers: string[] = [];  
  @State showResult: boolean = false;  
  @State isCorrect: boolean = false;  
  @State isLoading: boolean = true;  

  private courseId: string;  
  private quizModel = new QuizModel();  

  aboutToAppear() {  
    this.loadQuestions();  
  }  

  private async loadQuestions() {  
    this.isLoading = true;  
    try {  
      this.questions = await this.quizModel.getQuizQuestions(this.courseId);  
      this.isLoading = false;  
    } catch (error) {  
      console.error('Failed to load questions:', error);  
      this.isLoading = false;  
      // Add error prompt  
    }  
  }  

  private async submitAnswer() {  
    if (this.selectedAnswers.length === 0) {  
      // Prompt user to select an answer  
      return;  
    }  

    try {  
      const currentQuestion = this.questions[this.currentIndex];  
      this.isCorrect = await this.quizModel.submitAnswer(currentQuestion.id, this.selectedAnswers);  
      this.showResult = true;  
    } catch (error) {  
      console.error('Failed to submit answer:', error);  
      // Add error prompt  
    }  
  }  

  private nextQuestion() {  
    this.showResult = false;  
    this.selectedAnswers = [];  
    if (this.currentIndex < this.questions.length - 1) {  
      this.currentIndex++;  
    } else {  
      // Quiz completed, can navigate to results page  
      // or display summary information  
    }  
  }  

  build() {  
    Column() {  
      if (this.isLoading) {  
        LoadingProgress()  
          .width(50)  
          .height(50)  
      } else if (this.questions.length === 0) {  
        Text('No quiz questions available')  
          .fontSize(16)  
      } else {  
        // Question progress  
        Text(`Question ${this.currentIndex + 1}/${this.questions.length}`)  
          .fontSize(14)  
          .margin({ top: 10, bottom: 5 })  

        // Question content  
        Scroll() {  
          Column() {  
            // Question text  
            Text(this.questions[this.currentIndex].questionText)  
              .fontSize(18)  
              .fontWeight(FontWeight.Medium)  
              .margin({ bottom: 20 })  

            // Display different answer UI based on question type  
            if (this.questions[this.currentIndex].questionType === 'single' ||  
                this.questions[this.currentIndex].questionType === 'multiple') {  
              // Single or multiple-choice questions  
              ForEach(this.questions[this.currentIndex].options, (option: string, index: number) => {  
                Button(option)  
                  .width('90%')  
                  .margin({ bottom: 10 })  
                  .stateEffect(!this.showResult)  
                  .backgroundColor(  
                    this.showResult && this.questions[this.currentIndex].correctAnswers.includes(index.toString())  
                      ? '#4CAF50'  
                      : this.showResult && this.selectedAnswers.includes(index.toString()) && !this.isCorrect  
                        ? '#F44336'  
                        : this.selectedAnswers.includes(index.toString())  
                          ? '#2196F3'  
                          : '#FFFFFF'  
                  )  
                  .onClick(() => {  
                    if (this.showResult) return;  

                    const answer = index.toString();  
                    if (this.questions[this.currentIndex].questionType === 'single') {  
                      this.selectedAnswers = [answer];  
                    } else {  
                      if (this.selectedAnswers.includes(answer)) {  
                        this.selectedAnswers = this.selectedAnswers.filter(a => a !== answer);  
                      } else {  
                        this.selectedAnswers = [...this.selectedAnswers, answer];  
                      }  
                    }  
                  })  
              })  
            } else if (this.questions[this.currentIndex].questionType === 'true_false') {  
              // True/false questions  
              Row() {  
                Button('True')  
                  .width('40%')  
                  .backgroundColor(  
                    this.showResult && this.questions[this.currentIndex].correctAnswers.includes('true')  
                      ? '#4CAF50'  
                      : this.showResult && this.selectedAnswers.includes('true') && !this.isCorrect  
                        ? '#F44336'  
                        : this.selectedAnswers.includes('true')  
                          ? '#2196F3'  
                          : '#FFFFFF'  
                  )  
                  .onClick(() => {  
                    if (!this.showResult) this.selectedAnswers = ['true'];  
                  })  

                Button('False')  
                  .width('40%')  
                  .margin({ left: 20 })  
                  .backgroundColor(  
                    this.showResult && this.questions[this.currentIndex].correctAnswers.includes('false')  
                      ? '#4CAF50'  
                      : this.showResult && this.selectedAnswers.includes('false') && !this.isCorrect  
                        ? '#F44336'  
                        : this.selectedAnswers.includes('false')  
                          ? '#2196F3'  
                          : '#FFFFFF'  
                  )  
                  .onClick(() => {  
                    if (!this.showResult) this.selectedAnswers = ['false'];  
                  })  
              }  
              .width('100%')  
              .justifyContent(FlexAlign.Center)  
            } else {  
              // Fill-in-the-blank questions  
              TextInput({ placeholder: 'Enter your answer' })  
                .width('90%')  
                .height(100)  
                .margin({ bottom: 20 })  
                .onChange((value: string) => {  
                  this.selectedAnswers = [value];  
                })  
            }  

            // Answer explanation  
            if (this.showResult && this.questions[this.currentIndex].explanation) {  
              Text('Explanation: ' + this.questions[this.currentIndex].explanation)  
                .fontSize(14)  
                .margin({ top: 20, bottom: 10 })  
                .fontColor('#666666')  
            }  
          }  
          .padding(20)  
        }  
        .height('70%')  

        // Submit/next question button  
        Button(this.showResult ? 'Next Question' : 'Submit Answer')  
          .width('80%')  
          .height(50)  
          .margin({ top: 20 })  
          .onClick(() => {  
            if (this.showResult) {  
              this.nextQuestion();  
            } else {  
              this.submitAnswer();  
            }  
          })  
      }  
    }  
    .width('100%')  
    .height('100%')  
  }  
}  
Enter fullscreen mode Exit fullscreen mode

6. Learning Data Analysis and Visualization

Learning data analysis features can help users understand their learning progress. We can use AppGallery Connect's analytics service to implement this functionality.

1. Learning Data Statistics Implementation

// src/main/ets/model/AnalyticsModel.ts  
import { agconnect } from '@hw-agconnect/core-ohos';  
import { AGCAnalytics } from '@hw-agconnect/analytics-ohos';  
import { UserModel } from './UserModel';  

export class AnalyticsModel {  
  // Record learning behavior  
  static async logLearningEvent(courseId: string, chapterId: string, duration: number): Promise<void> {  
    try {  
      const user = await UserModel.getCurrentUser();  
      const userId = user ? user.getUid() : 'anonymous';  

      AGCAnalytics.getInstance().logEvent('learning_event', {  
        user_id: userId,  
        course_id: courseId,  
        chapter_id: chapterId,  
        duration: duration  
      });  
      console.info('Learning behavior recorded successfully');  
    } catch (error) {  
      console.error('Failed to record learning behavior:', error);  
    }  
  }  

  // Get user learning statistics  
  static async getUserLearningStats(userId: string): Promise<{  
    totalLearningTime: number,  
    courseProgress: Record<string, number>,  
    dailyLearning: Record<string, number>  
  }> {  
    // Note: In actual applications, analytics data should be retrieved via cloud functions  
    // Here, we return mock data for simplicity  
    return {  
      totalLearningTime: 1250, // minutes  
      courseProgress: {  
        'course_1': 65,  
        'course_2': 30,  
        'course_3': 90  
      },  
      dailyLearning: {  
        '2023-11-01': 45,  
        '2023-11-02': 60,  
        '2023-11-03': 30,  
        '2023-11-04': 90,  
        '2023-11-05': 45  
      }  
    };  
  }  
}  
Enter fullscreen mode Exit fullscreen mode

2. Learning Data Visualization UI

// src/main/ets/pages/AnalyticsPage.ets  
@Component  
struct AnalyticsPage {  
  @State learningStats: {  
    totalLearningTime: number,  
    courseProgress: Record<string, number>,  
    dailyLearning: Record<string, number>  
  } | null = null;  
  @State isLoading: boolean = true;  

  private analyticsModel = new AnalyticsModel();  

  aboutToAppear() {  
    this.loadLearningStats();  
  }  

  private async loadLearningStats() {  
    this.isLoading = true;  
    try {  
      const user = await UserModel.getCurrentUser();  
      if (!user) {  
        throw new Error('User not logged in');  
      }  
      this.learningStats = await AnalyticsModel.getUserLearningStats(user.getUid());  
      this.isLoading = false;  
    } catch (error) {  
      console.error('Failed to get learning data:', error);  
      this.isLoading = false;  
    }  
  }  

  build() {  
    Column() {  
      if (this.isLoading) {  
        LoadingProgress()  
          .width(50)  
          .height(50)  
      } else if (!this.learningStats) {  
        Text('Unable to get learning data')  
          .fontSize(16)  
      } else {  
        // Total learning time  
        Row() {  
          Text('Total learning time:')  
            .fontSize(16)  
          Text(`${Math.floor(this.learningStats.totalLearningTime / 60)} hours ${this.learningStats.totalLearningTime % 60} minutes`)  
            .fontSize(16)  
            .fontWeight(FontWeight.Bold)  
            .margin({ left: 10 })  
        }  
        .margin({ top: 20, bottom: 20 })  

        // Course progress chart  
        Text('Course Progress')  
          .fontSize(18)  
          .fontWeight(FontWeight.Medium)  
          .margin({ bottom: 10 })  

        ForEach(Object.entries(this.learningStats.courseProgress), ([courseId, progress]) => {  
          Column() {  
            Row() {  
              Text(`Course ${courseId.split('_')[1]}`)  
                .width('20%')  
                .fontSize(14)  

              Progress({  
                value: progress,  
                total: 100,  
                type: ProgressType.Linear  
              })  
                .width('70%')  
                .height(20)  
                .margin({ left: 10 })  

              Text(`${progress}%`)  
                .fontSize(14)  
                .margin({ left: 10 })  
            }  
            .margin({ bottom: 5 })  
          }  
        })  

        // Daily learning time chart  
        Text('Recent Learning Activity')  
          .fontSize(18)  
          .fontWeight(FontWeight.Medium)  
          .margin({ top: 20, bottom: 10 })  

        Row() {  
          ForEach(Object.entries(this.learningStats.dailyLearning), ([date, minutes]) => {  
            Column() {  
              // Bar chart  
              Column() {  
                Blank()  
              }  
              .width(30)  
              .height(minutes)  
              .backgroundColor('#2196F3')  
              .borderRadius(4)  

              // Date  
              Text(date.split('-')[2])  
                .fontSize(12)  
                .margin({ top: 5 })  
            }  
            .margin({ right: 5 })  
            .alignItems(HorizontalAlign.Center)  
          })  
        }  
        .height(150)  
        .margin({ top: 10 })  
        .justifyContent(FlexAlign.End)  
        .alignItems(VerticalAlign.Bottom)  
      }  
    }  
    .width('100%')  
    .height('100%')  
    .padding(20)  
  }  
}  
Enter fullscreen mode Exit fullscreen mode

7. Application Publishing and Continuous Integration

After development, we need to publish the application to AppGallery. AppGallery Connect provides comprehensive publishing and continuous integration capabilities.

1. Application Signing Configuration

Add signing configuration in build-profile.json5:

{  
  "app": {  
    "signingConfigs": [  
      {  
        "name": "release",  
        "material": {  
          "certpath": "signing/your_certificate.pem",  
          "storePassword": "your_store_password",  
          "keyAlias": "your_key_alias",  
          "keyPassword": "your_key_password",  
          "profile": "signing/your_profile.p7b",  
          "signAlg": "SHA256withECDSA",  
          "storeFile": "signing/your_keystore.jks"  
        }  
      }  
    ],  
    "buildType": "release"  
  }  
}  
Enter fullscreen mode Exit fullscreen mode

Top comments (0)