孔雀东南飞背诵APP开发实战:基于HarmonyOS API 24的ArkUI应用从零到一



用两千年前的经典叙事诗,练手最新的HarmonyOS原生开发——本文以《孔雀东南飞》背诵APP为案例,完整记录一个ArkUI(ArkTS)应用的架构设计、核心实现与踩坑实录。
一、项目缘起:为什么选《孔雀东南飞》做APP?
1.1 《孔雀东南飞》的文学价值与背诵需求
《孔雀东南飞》是中国古代最长的叙事诗,全诗357句、1785字,与《木兰诗》并称"乐府双璧"。它是中学语文教材中的必背篇目,也是许多中文专业学生的必修内容。然而,这样一首长诗的背诵是一项极具挑战性的任务——传统的纸质背诵方式缺乏交互反馈,学习者很难清晰地追踪自己的进度,也难以针对性地复习薄弱环节。
1.2 技术背景:HarmonyOS ArkUI与API 24
2024年,HarmonyOS NEXT(鸿蒙星河版)正式面向开发者开放,ArkUI作为其原生声明式UI框架,以ArkTS(基于TypeScript的鸿蒙原生语言)为开发语言,提供了一套完整的跨设备、高性能的UI开发方案。
API 24 是HarmonyOS NEXT的一个重要里程碑版本。在API 24中,ArkUI的组件能力有了显著增强:
| API 24 关键特性 | 说明 |
|---|---|
@State / @Prop / @Link 装饰器 |
响应式状态管理 |
@Builder 自定义构建函数 |
组件复用与模块化 |
ForEach 循环渲染 |
列表数据驱动UI |
List 组件高性能虚拟列表 |
长列表渲染优化 |
| 链式属性配置 | 声明式UI风格 |
| Stage模型 | 标准化应用生命周期 |
这些特性为我们开发一个结构清晰、交互流畅的背诵APP提供了坚实的技术基础。
1.3 本文目标
本文将通过一个完整的APP开发案例,系统地展示HarmonyOS API 24下ArkUI应用开发的完整流程,包括:
- 项目架构设计与数据组织
- 响应式状态管理的最佳实践
- 长列表渲染与组件复用
- 页面路由与视图切换
- 实战中的踩坑与解决思路
无论你是刚接触HarmonyOS开发的新手,还是有一定经验的开发者,都能从中获得可复用的经验和思路。
二、项目架构设计
2.1 需求分析
在动笔写代码之前,我们先梳理一下APP的核心需求:
功能需求:
- 展示全诗内容,按叙事段落合理分节
- 支持逐句揭示的背诵模式
- 可视化进度反馈(进度条、计数)
- 灵活的控制(重置、全部显示、单句切换)
- 段落间自由切换
非功能需求:
- 流畅的列表滚动性能(全诗近200句)
- 稳定的状态管理,不丢失进度
- 简洁优雅的UI风格,贴合古诗意境
2.2 技术选型
| 技术维度 | 选择 | 理由 |
|---|---|---|
| 开发语言 | ArkTS | HarmonyOS原生语言,静态类型安全 |
| UI框架 | ArkUI | 声明式、跨设备、原生性能 |
| 状态管理 | @State装饰器 |
内建响应式,无需引入第三方 |
| 数据组织 | 纯前端静态数据结构 | 内容固定,无需服务端 |
| API版本 | API 24 (HarmonyOS NEXT) | 最新稳定版,特性全面 |
2.3 架构分层
整个APP的架构可以分为三层:
┌──────────────────────────────────────────┐
│ 表现层 (UI Layer) │
│ List / Text / Button / Row / Column │
│ @Builder 自定义组件 │
├──────────────────────────────────────────┤
│ 状态层 (State Layer) │
│ @State currentView │
│ @State currentSectionIndex │
│ @State revealed / revealedCount │
├──────────────────────────────────────────┤
│ 数据层 (Data Layer) │
│ poemData[]: SectionData[] │
│ 全诗12段,共约200句 │
└──────────────────────────────────────────┘
数据层:静态嵌入全诗内容,按段落组织。
状态层:管理视图切换、当前段落、揭示状态。
表现层:根据状态渲染对应UI。
2.4 视图结构
APP采用单页面(Single Page)模式,通过条件渲染实现两个视图的切换:
App (Index.ets)
├── View 1: 段落列表视图 (SectionList)
│ ├── 标题区
│ ├── 进度总览
│ └── 段落卡片列表 (List + ForEach)
│
└── View 2: 背诵视图 (ReciteView)
├── 导航栏 (返回 + 标题 + 重置)
├── 进度条
├── 句子列表 (List + ForEach)
└── 底部操作栏 (全部显示 + 下一句)
三、数据组织:如何管理近200句古诗
3.1 诗文的段落划分
《孔雀东南飞》原文很长,如果将所有句子平铺展示,用户会感到无所适从。合理的分段是提升用户体验的第一步。
我们按照叙事单元将全诗划分为12个段落:
| 段落 | 标题 | 句数 | 主要内容 |
|---|---|---|---|
| 1 | 开篇 | 1 | 孔雀东南飞,五里一徘徊 |
| 2 | 兰芝诉苦 | 10 | 兰芝向焦仲卿倾诉苦衷 |
| 3 | 母子争执 | 16 | 焦母与焦仲卿的激烈冲突 |
| 4 | 夫妻话别 | 19 | 焦仲卿与刘兰芝的深情告别 |
| 5 | 严妆告别 | 17 | 兰芝盛装离开焦家 |
| 6 | 路口分别 | 13 | 路口立下磐石蒲苇之誓 |
| 7 | 回家与县令求婚 | 20 | 兰芝回家,县令提亲 |
| 8 | 太守求婚与兄逼 | 18 | 太守提亲,兄长逼迫 |
| 9 | 太守备婚 | 25 | 奢华备婚,兰芝绝望 |
| 10 | 生离死别 | 19 | 最后相见,相约赴死 |
| 11 | 府吏别母 | 14 | 焦仲卿诀别母亲 |
| 12 | 殉情化鸟 | 13 | 投水自缢,化鸟双飞 |
设计考量:每个段落10-25句,长度适中。用户每次进入一个段落,可以在一段时间内专注地完成该段的背诵,不会因过长而感到疲惫。
3.2 数据结构的定义
在ArkTS中,我们首先定义了一个接口来描述段落的数据结构:
interface SectionData {
title: string
lines: string[]
}
SectionData 非常简洁:title 是段落标题,lines 是诗句数组。这种设计使数据与UI完全解耦——我们可以轻松地增删改诗句内容而不影响UI逻辑。
3.3 全诗数据的组织
全诗数据被定义为一个数组,存储在组件的私有成员变量中:
private readonly poemData: SectionData[] = [
{
title: '开篇',
lines: ['孔雀东南飞,五里一徘徊。']
},
{
title: '兰芝诉苦',
lines: [
'"十三能织素,十四学裁衣。',
'十五弹箜篌,十六诵诗书。',
// ... 更多诗句
]
},
// ... 更多段落
];
3.4 为什么不用JSON文件?
在HarmonyOS项目中,数据也可以存放在 resources/rawfile 目录下的JSON文件中,通过网络请求或资源读取来获取。但在这个案例中,我们选择了直接嵌入到代码中,理由如下:
- 内容固定:全诗内容不会变化,不需要动态加载
- 零加载延迟:数据就在内存中,无IO开销
- 类型安全:编译器可以在编译时检查数据的类型正确性
- 简化构建:无需额外配置资源读取
对于需要联网更新或内容庞大的应用,采用外部JSON文件会是更好的选择。但对于本项目,嵌入代码是更优的方案。
四、状态管理:ArkUI响应式编程的核心
4.1 @State装饰器:驱动UI更新的引擎
ArkUI中的 @State 装饰器是响应式编程的核心。被 @State 修饰的变量发生变化时,依赖于该变量的UI组件会自动重新渲染。
在我们的APP中,定义了五个状态变量:
@State currentView: string = 'list'; // 当前视图
@State currentSectionIndex: number = 0; // 当前段落索引
@State revealed: boolean[] = []; // 句子揭示状态
@State totalLines: number = 0; // 当前段落总句数
@State revealedCount: number = 0; // 已揭示句数
4.2 状态设计的原则
在设计这些状态时,我们遵循了几个关键原则:
原则一:最小化状态
只有需要驱动UI变化的数据才定义为 @State。例如,全诗数据 poemData 是 private readonly 的,因为它不会发生变化,只需要被读取。
原则二:避免冗余状态
revealedCount 其实可以通过 revealed.filter(v => v).length 推导出来,为什么还要单独定义为 @State?
原因在于性能优化。如果每次渲染都动态计算 revealedCount,在每次状态变化时都会触发一次遍历。对于只有几十个元素的数组,这个开销可以忽略不计;但单独维护一个计数变量可以避免在 @Builder 方法中频繁进行数组操作。
原则三:状态的局部化
每个段落独立的 revealed 状态不是全局存储的(之前用Map的方案就是全局存储),而是每次进入段落时重新初始化。这样设计的好处是:
- 状态管理更简单,无需考虑跨段落的数据同步
- 内存占用更少,不需要同时保存所有段落的状态
- 逻辑更清晰,每个段落的行为是独立的
4.3 状态变更的完整链路
以"显示下一句"功能为例,状态变更的完整链路如下:
用户点击按钮
↓
revealNext() 被调用
↓
查找 revealed 中第一个 false 的位置
↓
const newRevealed = [...this.revealed];
newRevealed[nextIndex] = true;
↓
this.revealed = newRevealed; // 触发UI更新
this.revealedCount = ...; // 触发UI更新
↓
ArkUI检测到 @State 变化
↓
依赖 revealed / revealedCount 的 @Builder 重新执行
↓
List中的行项重新渲染
↓
用户看到新的句子显示出来
4.4 关键注意事项:数组的不可变性
在ArkUI中,@State 对数组的检测是引用级别的。也就是说,直接修改数组元素(如 this.revealed[i] = true)不会触发UI更新。
正确的做法是创建一个新数组:
// ❌ 错误:不会触发UI更新
this.revealed[nextIndex] = true;
// ✅ 正确:创建新数组,触发UI更新
const newRevealed = [...this.revealed];
newRevealed[nextIndex] = true;
this.revealed = newRevealed;
这个问题的本质是ArkUI的响应式系统追踪的是变量的引用地址,而不是变量内部的结构变化。这与React中的 setState 不可变数据理念是一致的。
4.5 视图切换的状态管理
APP的两个视图通过 currentView 状态来控制:
build() {
Column() {
if (this.currentView === 'list') {
this.buildSectionList();
} else {
this.buildReciteView();
}
}
// ...
}
enterRecite() 方法负责切换视图并初始化背诵状态:
enterRecite(index: number): void {
this.currentSectionIndex = index;
this.totalLines = this.poemData[index].lines.length;
this.revealed = new Array(this.totalLines).fill(false);
this.revealedCount = 0;
this.currentView = 'recite';
}
这里的关键是:在进入背诵模式时,一次性地初始化所有相关状态。这样做可以避免在后续操作中反复检查状态是否已初始化。
五、UI实现详解:从布局到组件复用
5.1 段落列表视图
段落列表是APP的首页,用户从这里选择要背诵的段落。它的布局结构如下:
Column (全屏)
├── Row: 标题 "孔雀东南飞 背诵"
├── Text: "汉乐府"
├── Row: 总进度 "共12段·总计XX句"
└── List: 段落卡片列表
├── ListItem: 卡片1
├── ListItem: 卡片2
└── ...
标题区使用 Row 水平排列主标题和副标题,字体颜色采用暗红色系(#8B0000),营造古风氛围。
段落卡片是列表的核心UI单元,每个卡片包含:
- 序号(带暗红底色)
- 标题 + 首句预览
- 句数标签
卡片使用 Column + Row 嵌套布局,配合 borderRadius 圆角和 shadow 阴影,营造出卡片浮起的视觉层次。
5.2 @Builder 组件复用
在ArkUI中,@Builder 装饰器用于定义可复用的构建函数。我们将段落卡片封装为一个 @Builder 方法:
@Builder
buildSectionCard(section: SectionData, index: number) {
Column() {
Row() {
Text(`${index + 1}`)
// 样式配置...
Column() {
Text(section.title) // ...
Text(section.lines[0]) // 首句预览
}
// ...
Text(`${section.lines.length}句`)
// 句数标签样式...
}
// ...
}
// 卡片整体样式与点击事件...
.onClick(() => {
this.enterRecite(index);
})
}
在 ForEach 中调用:
ForEach(this.poemData, (section: SectionData, index: number) => {
ListItem() {
this.buildSectionCard(section, index)
}
}, (item: SectionData, index: number) => index.toString())
@Builder的优势:
- 逻辑复用:相同的卡片结构在列表中被复用12次
- 结构清晰:将复杂的UI拆分为独立的构建函数
- 性能友好:ArkUI对 @Builder 的渲染进行了优化
5.3 背诵视图
背诵视图是APP的核心交互界面,布局如下:
Column (全屏)
├── Row: 导航栏 (←返回 | 段落标题 | 重置)
├── Row: 进度条 (计数 + 横条)
├── List: 句子列表 (可滚动)
│ ├── ListItem: 句子1 (已揭示/隐藏)
│ ├── ListItem: 句子2
│ └── ...
└── Row: 底部操作栏 (全部显示 | 显示下一句)
导航栏使用 Row 三栏布局,左右分别是"返回"和"重置"的文本按钮,中间是段落标题,通过 layoutWeight(1) 实现居中对齐。
进度条是一个嵌套的 Row 结构:
- 外层:显示"已背 X / Y 句"文本 + 进度条容器
- 内层:通过
width百分比控制填充宽度,颜色为暗红色
句子列表使用 List 组件,每个句子是一个独立的卡片项。
底部操作栏固定在屏幕底部,包含两个按钮。
5.4 句子的三种状态
每个句子在列表中呈现三种不同的视觉状态:
| 状态 | 视觉效果 | 可交互 |
|---|---|---|
| 未揭示 | "———— 点击显示"占位符,浅灰底色 | ✅ 点击揭示 |
| 已揭示 | 原文显示,白色底色 | ❌ 不可再点击 |
| 全部揭示后 | 所有原文显示,按钮变为"✓ 已背完" | — |
状态切换通过 @State revealed 数组实现:
@Builder
buildLineItem(line: string, lineIndex: number) {
Column() {
if (this.revealed[lineIndex]) {
// 已揭示:显示原文
Text(line)
.fontSize(17)
.fontColor('#333')
// ...
} else {
// 隐藏状态:显示占位符
Row() {
Text('————')
.fontColor('#CCC')
Text('点击显示')
.fontColor('#DDD')
}
}
// 序号
Text(`${lineIndex + 1}`)
// ...
}
.onClick(() => {
if (!this.revealed[lineIndex]) {
this.toggleLine(lineIndex);
}
})
}
5.5 条件渲染的多种形式
APP中使用了多种条件渲染形式:
1. 视图级别的条件渲染:
if (this.currentView === 'list') {
this.buildSectionList();
} else {
this.buildReciteView();
}
2. 组件级别的条件渲染:
if (section.lines.length > 0) {
Text(section.lines[0]) // 首句预览
}
3. 属性级别的条件渲染(三元表达式):
.backgroundColor(this.revealed[lineIndex] ? '#FFFFFF' : '#FAFAFA')
Text(this.revealedCount < this.totalLines ? '显示下一句 ▸' : '✓ 已背完')
条件渲染的灵活运用使UI能够根据状态自动切换,而无须手动操作DOM,这正是声明式UI的核心优势。
六、List组件的使用与性能优化
6.1 为什么选择List而不是Column
在背诵视图中,我们需要展示一个段落中的所有句子(最多25句)。如果使用 Column 包裹所有句子:
Column() {
ForEach(lines, (line) => { Text(line) })
}
所有的句子都会一次性渲染到组件树中,无论它们是否在屏幕上可见。对于几十个句子的长度,这种方式的性能开销还可以接受。
但 List 组件提供了更优的方案:
List() {
ForEach(lines, (line, index) => {
ListItem() {
this.buildLineItem(line, index)
}
})
}
.layoutWeight(1) // 填充可用空间
List的优势:
- 虚拟列表:只渲染屏幕可见区域的项目,滚动时动态回收和创建
- 内置滚动:自动处理触摸滚动、惯性滑动
- layoutWeight:配合父容器实现弹性布局
- 分隔线支持:通过
ListItem的样式实现
6.2 ForEach的key生成器
在列表渲染中,ForEach 的第三个参数是key生成器:
ForEach(
this.poemData,
(section, index) => { /* ... */ },
(item, index) => index.toString() // key生成器
)
key生成器的作用是帮助ArkUI识别列表中的每一项,从而在列表更新时进行最小化的DOM diff。在我们的场景中,数据是静态的(不会增删排序),使用 index 作为key足够了。但如果列表支持动态增删排序,应该使用唯一标识符作为key。
6.3 列表性能优化要点
对于背诵APP中的列表,我们采取了以下优化措施:
1. 避免嵌套太深:每个 ListItem 只包含一个Column,Column内是Text或Row,尽量保持组件树的扁平化。
2. 减少不必要的状态依赖:buildLineItem 中只依赖 this.revealed[lineIndex] 单一状态,避免依赖整个 revealed 数组。
3. 使用不可变数据:每次状态变更都创建新的数组,让ArkUI能够通过引用比较快速判断是否需要重新渲染。
4. 适度的列表长度:每个段落控制在25句以内,避免单个List承载过多的项目。
七、UI风格与视觉设计
7.1 配色方案
APP的配色方案围绕"古风"这个核心调性展开:
| 用途 | 色值 | 色系 |
|---|---|---|
| 主色/强调色 | #8B0000 |
暗红(古典朱砂) |
| 页面背景 | #FFF8F0 |
米白(古纸色) |
| 卡片背景 | #FFFFFF |
纯白 |
| 主文字 | #333333 |
深灰 |
| 次要文字 | #888888 |
中灰 |
| 弱化文字 | #AAAAAA / #CCCCCC |
浅灰 |
| 标签底色 | #FFF0E0 |
浅橙红 |
| 完成色 | #4CAF50 |
绿色(完成状态) |
暗红色 #8B0000 是所有交互元素(标题、序号、按钮、进度条)的主色调,它让人联想到朱砂、印章、古籍封面,与古诗的主题高度契合。
7.2 字体与排版
- 标题:28fp/18fp,加粗
- 正文诗句:17fp,常规字重,行高28px
- 辅助信息:12-14fp,浅灰色
文字层级清晰,用户在背诵时可以舒适地阅读长文本。
7.3 圆角与阴影
卡片和按钮使用圆角设计:
.borderRadius(12) // 卡片圆角
.borderRadius(8) // 句子项圆角
.borderRadius(20) // 按钮圆角
卡片阴影使用 shadow API:
.shadow({ radius: 4, color: '#20000000', offsetX: 0, offsetY: 2 })
这里的 color 使用了带透明度的黑色(#20000000),产生柔和的光影效果,避免生硬的阴影边缘。
八、踩坑记录与解决方案
以下是在开发过程中遇到的一些典型问题及其解决方案,这些经验可以帮助后来者少走弯路。
8.1 坑一:@State Map不触发UI更新
问题描述:
最初,我用 Map<number, boolean[]> 来存储所有段落的揭示状态:
@State revealedMap: Map<number, boolean[]> = new Map();
每次想要更新某个段落的某句揭示状态时:
this.revealedMap.set(sectionIndex, newArray);
但UI没有更新。
原因分析:
@State 装饰器对 Map 对象的检测是引用级别的。map.set() 不会改变 Map 对象的引用地址,因此ArkUI无法检测到变化。
解决方案:
改用更简单的 boolean[] 数组,每次变更时创建新数组替换:
// 每次进入段落时初始化
this.revealed = new Array(this.totalLines).fill(false);
// 变更时创建新数组
const newRevealed = [...this.revealed];
newRevealed[index] = true;
this.revealed = newRevealed;
教训:@State 应尽量使用基本类型(string、number、boolean)或纯数组,避免使用 Map、Set 等复杂集合类型。
8.2 坑二:@Builder方法中的this指向
问题描述:
在 @Builder 方法内部调用组件的方法(如 this.toggleLine())时,有时会出现 this 指向错误。
原因分析:
@Builder 方法中的 this 指向的是所属的组件实例,这是正确的。但是如果在 @Builder 内部使用箭头函数时,箭头函数的 this 会捕获定义时的上下文。
解决方案:
使用箭头函数而非普通函数作为回调:
// ✅ 正确:箭头函数保持this指向
.onClick(() => {
this.toggleLine(lineIndex);
})
// ❌ 错误:普通函数中this会丢失
// .onClick(function() {
// this.toggleLine(lineIndex); // this不指向组件实例
// })
8.3 坑三:ForEach的key警告
问题描述:
在某些版本的IDE中,ForEach 不提供key生成器参数时,会输出黄色警告。
解决方案:
始终为 ForEach 提供第三个参数——key生成器:
ForEach(
dataArray,
(item, index) => { /* UI构建 */ },
(item, index) => index.toString() // 使用index作为key
)
8.4 坑四:@Builder方法不能被导出
问题描述:
尝试将 @Builder 方法定义在单独的 .ets 文件中并导出,编译时报错。
原因分析:
在ArkUI中,@Builder 方法与所属的组件绑定,不能被独立导出和复用。如果需要在多个组件间共享构建逻辑,应该使用自定义组件(@Component)。
解决方案:
将复用逻辑定义在组件内部作为 @Builder 方法,或提取为独立的 @Component 自定义组件。
8.5 坑五:List组件的layoutWeight使用
问题描述:
List 组件在使用 layoutWeight 时,如果父容器高度不确定,可能导致列表无法正确填充空间。
解决方案:
确保 List 的所有父容器都有明确的高度约束:
Column() { // 父容器需要有固定高度或100%
// ...
List() {
// ...
}
.layoutWeight(1) // 填充剩余空间
}
.width('100%')
.height('100%') // 关键:父容器指定高度
8.6 坑六:字符串拼接中的类型安全
问题描述:
在ArkTS中,${expression} 模板字符串中的表达式必须确保类型正确。例如,计算进度百分比时:
.width(`${this.revealedCount / this.totalLines * 100}%`)
如果 this.totalLines 为0(尚未初始化),会得到 Infinity% 这样的非法值。
解决方案:
加上保护判断:
.width(`${this.totalLines > 0 ? (this.revealedCount / this.totalLines * 100) : 0}%`)
九、AppScope配置与项目构建
9.1 应用级配置
在 AppScope/app.json5 中配置应用的全局信息:
{
"app": {
"bundleName": "com.example.myapplication",
"vendor": "example",
"versionCode": 1000000,
"versionName": "1.0.0",
"icon": "$media:layered_image",
"label": "$string:app_name"
}
}
9.2 模块级配置
在 entry/src/main/module.json5 中配置模块的Ability信息:
{
"module": {
"name": "entry",
"type": "entry",
"pages": "$profile:main_pages",
"abilities": [
{
"name": "EntryAbility",
"srcEntry": "./ets/entryability/EntryAbility.ets",
"description": "$string:EntryAbility_desc",
"icon": "$media:layered_image",
"label": "$string:EntryAbility_label",
"startWindowIcon": "$media:startIcon",
"startWindowBackground": "$color:start_window_background",
"exported": true,
"skills": [
{
"entities": ["entity.system.home"],
"actions": ["ohos.want.action.home"]
}
]
}
]
}
}
关键配置项说明:
- pages:指定页面路由配置文件的路径
- abilities:注册Ability,设置入口页面、图标、标签等
- skills:声明Ability能够处理的意图,
entity.system.home表示桌面图标入口
9.3 资源文件管理
字符串资源(string.json)用于管理应用中所有的文本内容:
{
"string": [
{ "name": "app_name", "value": "孔雀东南飞背诵" },
{ "name": "module_desc", "value": "孔雀东南飞背诵助手" },
{ "name": "EntryAbility_desc", "value": "逐句背诵经典汉乐府" },
{ "name": "EntryAbility_label", "value": "孔雀东南飞" }
]
}
使用 $string:app_name 引用资源,而非硬编码字符串,是HarmonyOS推荐的最佳实践,有利于后续多语言适配。
9.4 页面路由配置
main_pages.json 中注册应用的所有页面:
{
"src": ["pages/Index"]
}
对于单页面应用,只需要注册一个页面入口。如果APP扩展为多页面,在此添加新的页面路径即可。
十、测试与调试经验
10.1 预览器的使用
HarmonyOS的DevEco Studio提供了 Previewer 功能,可以在不连接真机的情况下预览UI效果。在开发过程中,我们频繁使用Previewer来验证UI布局和交互效果。
使用技巧:
- 修改代码后 Previewer 会自动刷新
- 支持多设备形态预览(手机、平板等)
- 可以模拟点击和滚动交互
10.2 真机调试
真机调试是验证应用性能的关键环节,特别需要注意:
- 列表滚动流畅度:在真机上测试
List组件的滚动性能 - 触摸响应:按钮点击的反馈延迟
- 字体显示:中文字体在不同屏幕密度下的渲染效果
- 状态保持:应用切到后台再切回,状态是否正确保持
10.3 常见构建问题
问题1:同步失败
删除 oh_modules 目录,重启 DevEco Studio 重新同步。
问题2:语法报错但代码看起来没问题
检查 ArkTS 的静态类型要求。ArkTS比标准TypeScript更加严格:
- 必须显式声明类型
- 禁止
any类型 - 变量类型不可变
问题3:API接口不存在
确认 build-profile.json5 中配置的 API 版本是否正确:
{
"apiType": "stageMode",
"buildOption": { /* ... */ },
"targets": [{ "name": "default" }]
}
十一、项目代码结构全景
最后,让我们从全局视角回顾整个项目的代码结构:
MyApplication/
├── AppScope/
│ ├── app.json5 # 应用级配置
│ └── resources/base/element/
│ └── string.json # 字符串资源
│
├── entry/
│ ├── src/main/
│ │ ├── ets/
│ │ │ ├── entryability/
│ │ │ │ └── EntryAbility.ets # Ability入口
│ │ │ └── pages/
│ │ │ └── Index.ets # 主页面(574行)
│ │ ├── module.json5 # 模块配置
│ │ └── resources/
│ │ ├── base/element/
│ │ │ ├── color.json
│ │ │ ├── float.json
│ │ │ └── string.json
│ │ └── base/profile/
│ │ └── main_pages.json # 页面路由
│ ├── build-profile.json5
│ └── oh-package.json5
│
├── hvigor/
│ └── hvigor-config.json5
├── build-profile.json5
└── oh-package.json5
整个应用的核心代码集中在 Index.ets 这一个文件中(574行),包括:
- 1个接口定义(SectionData)
- 1个组件定义(Index)
- 5个 @State 状态变量
- 12段全诗数据(约200句)
- 10个方法
- 5个 @Builder 构建方法
十二、总结与展望
12.1 通过这个项目学到了什么
-
ArkUI的声明式编程范式:通过
@State+ 条件渲染 +@Builder的组合,可以高效地构建交互式UI应用。 -
响应式状态管理:理解
@State的工作原理和限制(引用比较、不可变数据),写出正确且高效的状态更新逻辑。 -
组件复用策略:
@Builder自定义构建函数和@Component自定义组件的适用场景与取舍。 -
长列表渲染:
List+ForEach的配合使用,以及key生成器的重要性。 -
HarmonyOS项目配置:从 AppScope 到 module.json5 到页面的完整配置链路。
12.2 可以继续优化的方向
虽然当前版本的APP已经实现了核心功能,但仍有许多可以优化的方向:
-
添加音频朗读:利用HarmonyOS的音频播放API,为每句诗添加配音,实现"听背"结合。
-
AI辅助评分:接入语音识别API,让用户朗读诗句并自动评分。
-
多设备协同:利用HarmonyOS的分布式能力,在手机和平板之间同步背诵进度。
-
间隔复习算法:引入艾宾浩斯遗忘曲线算法,智能安排复习计划。
-
动画效果:为句子揭示添加渐变、滑动等过渡动画,提升交互体验。
12.3 最后的一些话
从一首两千年前的长诗,到一个运行在手机上的APP,这中间跨越的是漫长的时间,但不变的是人们对美好事物的向往和传承。
《孔雀东南飞》中刘兰芝和焦仲卿用生命守护了他们的爱情誓言,而我们的APP用户用日复一日的背诵守护着这份文化遗产。作为一个技术产品,能够参与到这种文化传承中,是我们的荣幸。
HarmonyOS的ArkUI框架正在快速迭代中,期待在未来看到更多基于鸿蒙生态的优秀文化类应用诞生。希望本文能够为正在探索ArkUI开发的你提供一些有价值的参考。
附录:核心源码速览
以下是 APP 中 10 个关键方法/组件的快速索引,方便你在自己的项目中参考。
入口与数据定义
interface SectionData { title: string; lines: string[] }
@Entry @Component struct Index {
@State currentView: string = 'list';
@State currentSectionIndex: number = 0;
@State revealed: boolean[] = [];
@State totalLines: number = 0;
@State revealedCount: number = 0;
private readonly poemData: SectionData[] = [ /* 12段全诗数据 */ ];
}
核心交互方法
| 方法 | 作用 | 关键代码 |
|---|---|---|
enterRecite() |
进入背诵模式 | this.revealed = new Array(n).fill(false) |
revealNext() |
揭示下一句 | const newR = [...this.revealed]; newR[idx]=true; this.revealed=newR |
toggleLine() |
切换单句 | newR[lineIndex] = !newR[lineIndex] |
resetSection() |
重置段落 | this.revealed = new Array(n).fill(false) |
revealAll() |
全部显示 | this.revealed = new Array(n).fill(true) |
UI核心结构
build() {
Column() {
if (this.currentView === 'list') {
this.buildSectionList(); // @Builder 段落列表
} else {
this.buildReciteView(); // @Builder 背诵视图
}
}
.height('100%').width('100%')
.backgroundColor('#FFF8F0')
}
更多推荐


所有评论(0)