Mobile Development 13 min read

Building and Publishing a HarmonyOS Atomic Service: Daily Quotes Case Study

This tutorial walks through the complete lifecycle of a HarmonyOS atomic service — from UI implementation with Navigation, Grid, LazyForEach, and @Builder, to AI-powered text-to-speech, and finally manual code signing, packaging, and release via AppGallery Connect.

51CTO HarmonyOS Developer Community
51CTO HarmonyOS Developer Community
51CTO HarmonyOS Developer Community
Building and Publishing a HarmonyOS Atomic Service: Daily Quotes Case Study

Project Overview

"Daily Quotes" is a HarmonyOS atomic service that displays a new quote each day. Users can swipe to refresh, search by keyword, and listen to quotes via text-to-speech. The service is built with HarmonyOS 6 (API 12) using ArkTS.

Core Technical Implementation

1. Navigation for Page Switching

The main page Index.ets uses a Navigation component with a NavPathStack controller in Stack mode and hidden title bar:

pageInfos: NavPathStack = new NavPathStack();
build() {
  Navigation(this.pageInfos) {
    // ...
  }
  .mode(NavigationMode.Stack)
  .hideTitleBar(true)
}

2. Grid and Stack for Home Layout

A Stack places a full-screen background Image at the bottom. Above it, a Scroll contains a Column with a search Row and a Grid for quote cards:

Stack() {
  Image($r('app.media.bg'))
    .objectFit(ImageFit.Fill)
    .width('100%')
    .height('100%')
  Scroll() {
    Column() {
      Row() {
        Search({ value: this.searchContent, placeholder: '关键字搜索...', controller: this.controller })
          .searchIcon(new SymbolGlyphModifier($r('sys.symbol.magnifyingglass')).fontColor([$r('app.color.main_background')]))
          .cancelButton({ style: CancelButtonStyle.CONSTANT, icon: new SymbolGlyphModifier($r('sys.symbol.xmark')).fontColor([Color.Green]) })
          .searchButton('搜索')
          .width('70%')
          .height(40)
          .backgroundColor('#F5F5F5')
          .placeholderColor(Color.Grey)
          .placeholderFont({ size: 14, weight: 400 })
          .textFont({ size: 14, weight: 400 })
          .margin(20)
          .onSubmit((value: string) => {
            this.searchContent = value;
            this.doSearch(this.searchContent);
          })
      }.width('100%').justifyContent(FlexAlign.Start)
      Grid(this.scroller, this.layoutOptions) {
        // LazyForEach items rendered here
      }
      .columnsGap(8)
      .rowsGap(8)
      .columnsTemplate('1fr')
      .width('100%')
      .height('100%')
      .padding({ left: 10, right: 10, bottom: 40 })
    }
    .width('100%')
    .height('100%')
    .margin({ bottom: 10 })
  .width('100%')
  .scrollBar(BarState.Off)
}

The Stack layers the background image beneath the scrollable content. The Grid uses a single-column template with 8px gaps.

3. LazyForEach for Lazy Data Loading

The Grid renders quote cards via LazyForEach, which takes a data source array and a render function. Each item is a GridItem showing title and author in a Text component. Clicking pushes the detail page onto the navigation stack:

Grid(this.scroller, this.layoutOptions) {
  LazyForEach(this.dataSource, (item: Poet) => {
    GridItem() {
      Text(`${item.title}(${item.author})`)
        .width('100%')
        .height(28)
        .margin(10)
        .fontColor($r('sys.color.font_primary'))
        .fontSize(24)
        .maxLines(1)
        .textOverflow({ overflow: TextOverflow.Ellipsis })
        .onClick(() => {
          this.pageInfos.pushPathByName('PageDetail', item);
          console.log('切换到Detail页面');
        })
    }
  }, (item: Poet) => item.id + '')
}
LazyForEach

automatically creates components only for visible items, improving performance for long lists.

4. @Builder for Custom Detail Page

A @Builder function TextLineBuilder constructs the detail view. It uses a Stack with a background image and a centered Column containing title, author, and paragraphs via ForEach:

@Builder
function TextLineBuilder(poet: Poet, color: string, index: number) {
  if (poet) {
    Stack() {
      Image($r('app.media.type_' + index))
        .objectFit(ImageFit.Fill)
        .width('100%')
        .height('100%')
      Column() {
        Text(poet.title)
          .fontSize(24)
          .fontWeight(FontWeight.Bold)
          .fontColor(color)
          .backgroundColor(Color.White)
        Text(poet.author)
          .fontSize(20)
          .fontWeight(FontWeight.Medium)
          .fontColor(color)
          .backgroundColor(Color.White)
        ForEach(poet.paragraphs, (paragraph: string, index: number) => {
          Text(paragraph)
            .fontSize(22)
            .fontWeight(FontWeight.Lighter)
            .fontColor(color)
            .backgroundColor(Color.White)
        })
      .width('100%')
      .height('100%')
      .justifyContent(FlexAlign.Center)
    }
    .width('100%')
    .height('100%')
  }
}

5. Scene-Based Voice Service for Text-to-Speech

A "Read Aloud" button invokes TextReader.start with a structured readInfoList containing the quote's id, title, author, and body text (paragraphs joined by newline). Errors are caught and logged:

Button('朗读').onClick(() => {
  this.readInfoList = [
    {
      id: this.poet.id + '',
      title: { text: this.poet.title, isClickable: true },
      author: { text: this.poet.author, isClickable: true },
      bodyInfo: this.poet.paragraphs.join('
')
    }
  ];
  this.selectedReadInfo = this.readInfoList[0];
  TextReader.start(this.readInfoList, this.selectedReadInfo?.id)
    .catch((error: BusinessError) => {
      console.error(`start failed, code: ${error.code}, message: ${error.message}`);
    });
}).margin(10)

Publishing Process: From Development to Release

Key Concepts

Keystore (.p12) : Stores asymmetric key pair (public/private) for digital signing.

CSR (.csr) : Certificate Signing Request containing public key and identity info, submitted to AppGallery Connect.

Digital Certificate (.cer) : Issued by Huawei AppGallery Connect.

Profile (.p7b) : Contains package name, certificate info, requested permissions, and allowed debug devices (empty for Release).

Step-by-Step Signing & Release

1. Generate Keystore

In DevEco Studio: set path (e.g., D:\dev\harmonyos\keystore\birlower_20250916.p12), password, and alias (e.g., birlower_20250916).

2. Generate CSR

Based on the keystore, create .csr file (e.g., D:\dev\harmonyos\keystore\birlower_20250916.csr).

3. Request Digital Certificate

Log in to AppGallery Connect → Digital Certificates → Add New. Fill certificate name (e.g., birlower_20250916), select "Release Certificate", upload CSR. Download the issued .cer.

4. Create Profile

In AppGallery Connect → Profiles → Add Profile. Name (e.g., birlower_20250916), type "Release", select the certificate from step 3. Download .p7b.

5. Manual Signing Configuration

In DevEco Studio: File > Project Structure > Project > Signing Configs, uncheck "Automatically generate signature", then fill:

Store file : keystore .p12 from step 1

Store password : keystore password

Key alias : alias from step 1

Key password : same as store password

Sign alg : fixed to SHA256withECDSA Profile file : .p7b from step 4

Certpath file : .cer from step 3

6. Build Signed APP

Run Build > Build Hap(s)/APP(s) > Build APP(s). Default mode is Release. Output appears at

build/outputs/default/DailyQuotesAtomicService-default-signed.app

.

7. Upload to AppGallery Connect

In AGC Software Package Management, upload the signed .app for test or formal release.

8. Distribute

In AGC Version Information, select the uploaded package. Fill multilingual descriptions (plain text only, no HTML). Set privacy policy (hosted privacy policy recommended). Submit for review.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

HarmonyOSNavigationText-to-SpeechArkTSAppGallery ConnectCode SigningLazyForEachAtomic Service
51CTO HarmonyOS Developer Community
Written by

51CTO HarmonyOS Developer Community

The HarmonyOS Developer Community is a learning-oriented community for developers to learn, communicate, ask questions, and share.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.