在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

用两千年前的经典叙事诗,练手最新的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的核心需求:

功能需求:

  1. 展示全诗内容,按叙事段落合理分节
  2. 支持逐句揭示的背诵模式
  3. 可视化进度反馈(进度条、计数)
  4. 灵活的控制(重置、全部显示、单句切换)
  5. 段落间自由切换

非功能需求:

  1. 流畅的列表滚动性能(全诗近200句)
  2. 稳定的状态管理,不丢失进度
  3. 简洁优雅的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文件中,通过网络请求或资源读取来获取。但在这个案例中,我们选择了直接嵌入到代码中,理由如下:

  1. 内容固定:全诗内容不会变化,不需要动态加载
  2. 零加载延迟:数据就在内存中,无IO开销
  3. 类型安全:编译器可以在编译时检查数据的类型正确性
  4. 简化构建:无需额外配置资源读取

对于需要联网更新或内容庞大的应用,采用外部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。例如,全诗数据 poemDataprivate 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的优势:

  1. 逻辑复用:相同的卡片结构在列表中被复用12次
  2. 结构清晰:将复杂的UI拆分为独立的构建函数
  3. 性能友好: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的优势:

  1. 虚拟列表:只渲染屏幕可见区域的项目,滚动时动态回收和创建
  2. 内置滚动:自动处理触摸滚动、惯性滑动
  3. layoutWeight:配合父容器实现弹性布局
  4. 分隔线支持:通过 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 应尽量使用基本类型(stringnumberboolean)或纯数组,避免使用 MapSet 等复杂集合类型。

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 真机调试

真机调试是验证应用性能的关键环节,特别需要注意:

  1. 列表滚动流畅度:在真机上测试 List 组件的滚动性能
  2. 触摸响应:按钮点击的反馈延迟
  3. 字体显示:中文字体在不同屏幕密度下的渲染效果
  4. 状态保持:应用切到后台再切回,状态是否正确保持

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 通过这个项目学到了什么

  1. ArkUI的声明式编程范式:通过 @State + 条件渲染 + @Builder 的组合,可以高效地构建交互式UI应用。

  2. 响应式状态管理:理解 @State 的工作原理和限制(引用比较、不可变数据),写出正确且高效的状态更新逻辑。

  3. 组件复用策略@Builder 自定义构建函数和 @Component 自定义组件的适用场景与取舍。

  4. 长列表渲染List + ForEach 的配合使用,以及key生成器的重要性。

  5. HarmonyOS项目配置:从 AppScope 到 module.json5 到页面的完整配置链路。

12.2 可以继续优化的方向

虽然当前版本的APP已经实现了核心功能,但仍有许多可以优化的方向:

  1. 添加音频朗读:利用HarmonyOS的音频播放API,为每句诗添加配音,实现"听背"结合。

  2. AI辅助评分:接入语音识别API,让用户朗读诗句并自动评分。

  3. 多设备协同:利用HarmonyOS的分布式能力,在手机和平板之间同步背诵进度。

  4. 间隔复习算法:引入艾宾浩斯遗忘曲线算法,智能安排复习计划。

  5. 动画效果:为句子揭示添加渐变、滑动等过渡动画,提升交互体验。

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')
}
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐