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:
- Install the latest version of DevEco Studio (recommended version 4.0 or higher).
- Register a Huawei Developer account and complete real-name verification.
- 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);
}
});
}
}
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;
}
}
}
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;
}
}
}
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;
}
}
}
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 })
}
}
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;
}
}
}
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%')
}
}
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
}
};
}
}
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)
}
}
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"
}
}
Top comments (0)