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.
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 + '')
} LazyForEachautomatically 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.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
51CTO HarmonyOS Developer Community
The HarmonyOS Developer Community is a learning-oriented community for developers to learn, communicate, ask questions, and share.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
