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

在这里插入图片描述

在这里插入图片描述

目录

  1. 项目缘起
  2. 技术选型与架构设计
  3. 开发环境搭建
  4. 项目结构规划
  5. 数据层:诗文的数字化建模
  6. 页面层:三Tab框架的实现
  7. 全文浏览模式
  8. 提示背诵模式
  9. 逐句默写模式
  10. 编译构建与签名
  11. 踩坑记录与最佳实践
  12. 总结与展望

一、项目缘起

《孔雀东南飞》是中国文学史上第一部长篇叙事诗,被誉为"乐府双璧"之一。全诗三百五十七句,一千七百余字,讲述了刘兰芝与焦仲卿的爱情悲剧。如此经典的文学作品,至今没有一个专门为背诵设计的移动端工具。

这个项目就是为了填补这个空白——用 HarmonyOS ArkTS 构建一个轻量级的古诗背诵助手。目标很简单:让用户能在手机上反复诵读、练习默写,直至全文成诵。

项目从立项到完成,核心代码只有两个文件、约 830 行代码,充分体现了 ArkTS 在快速原型开发上的效率。


二、技术选型与架构设计

2.1 为什么选择 HarmonyOS ArkTS

选择 ArkTS 有几个务实的理由:

因素 说明
声明式UI ArkTS 的 @Component + @Builder 模型与 SwiftUI / Jetpack Compose 类似,学习曲线平缓
单语言覆盖 逻辑层和视图层都用 TypeScript 语法,无需在 Kotlin/Swift/Java 之间切换
Stage 模型 API 24 强制 Stage 模型,生命周期管理更清晰
原生性能 ArkTS 编译为方舟字节码,不依赖 WebView,滚动大量文本也没有卡顿

2.2 整体架构

应用采用单页面多视图架构,而非多页面路由。原因有二:一是功能简单,三个Tab之间的切换无需复杂的路由栈管理;二是保持状态共享的方便性——三个Tab共享同一份诗词数据。

┌──────────────────────────────────┐
│          App (Index.ets)          │
│  ┌────────────────────────────┐   │
│  │     标题栏                  │   │
│  ├────────────────────────────┤   │
│  │   Tab 导航 (全文/背诵/默写) │   │
│  ├────────────────────────────┤   │
│  │                            │   │
│  │   ┌─── Tab 0: 全文浏览 ──┐ │   │
│  │   │   Scroll + ForEach    │ │   │
│  │   │   POEM_SECTIONS       │ │   │
│  │   └──────────────────────┘ │   │
│  │                            │   │
│  │   ┌─── Tab 1: 提示背诵 ──┐ │   │
│  │   │   按段背诵 / 逐句背诵  │ │   │
│  │   │   showLineText toggle │ │   │
│  │   └──────────────────────┘ │   │
│  │                            │   │
│  │   ┌─── Tab 2: 逐句默写 ──┐ │   │
│  │   │   TextArea + 自动判对 │ │   │
│  │   └──────────────────────┘ │   │
│  └────────────────────────────┘   │
└──────────────────────────────────┘
         ↑      数据层     ↓
   ┌────────────────────────────┐
   │   PoemData.ets (数据模型)   │
   │   POEM_SECTIONS[]          │
   │   getAllLines()            │
   └────────────────────────────┘

2.3 核心依赖

项目的 oh-package.json5 十分干净,没有任何第三方依赖:

{
  "dependencies": {}
}

所有 UI 组件全部使用 ArkUI 框架内置组件,零外部引入。这对小型应用来说是一个显著的优势——不必操心版本兼容和包体积膨胀。


三、开发环境搭建

3.1 工具链版本

组件 版本
DevEco Studio 6.1.x(推荐最新)
HarmonyOS SDK API 24 (6.1.1)
ArkTS 版本 基于 TypeScript 5.0+
构建工具 hvigor 6.1.1

3.2 项目初始化要点

创建项目时选择 Empty Ability 模板,API 选择 24,模型选择 Stage。生成骨架后需要做两件事:

  1. 确认 build-profile.json5targetSdkVersioncompatibleSdkVersion"6.1.1(24)"
  2. 确认 buildModeSet 包含 debugrelease
{
  "products": [{
    "name": "default",
    "targetSdkVersion": "6.1.1(24)",
    "compatibleSdkVersion": "6.1.1(24)",
    "runtimeOS": "HarmonyOS"
  }]
}

这里有一个容易忽略的细节:runtimeOS 字段必须明确声明为 "HarmonyOS",否则在打包时可能失败。

3.3 目录结构

项目初始化后,需要手动创建 models/ 目录来存放数据文件:

entry/src/main/ets/
├── entryability/
│   └── EntryAbility.ets     # Ability生命周期入口(自动生成)
├── models/
│   └── PoemData.ets          # 诗词数据和类型定义(手动创建)
└── pages/
    └── Index.ets             # 主页面(手动重写)

需要注册页面路由:在 resources/base/profile/main_pages.json 中:

{
  "src": ["pages/Index"]
}

四、项目结构规划

4.1 文件职责划分

这是一个非常简洁的结构,只有两个核心文件:

PoemData.ets    ← 纯数据层,零UI依赖
Index.ets       ← 纯UI层,消费数据层

这种分离的好处显而易见:

  • 数据层可测试PoemData.ets 可以独立运行,其导出的函数可以在单元测试中直接调用
  • UI层可替换:如果需要换用不同的UI框架(比如从 ArkTS 换到 Java UI),数据层完全不需要改动
  • 模块化边界清晰Index.ets 通过 import 消费数据,不直接修改数据,符合单向数据流原则

4.2 为什么不使用多页面路由

这个应用只有三个功能Tab,且它们共享同一份诗词数据。如果拆成三个独立的 @Page

  1. 要么把数据放在全局(污染命名空间)
  2. 要么在页面间通过 router.push 传参(数据量太大,URL 参数有长度限制)
  3. 要么使用 AppStorage / LocalStorage(增加了复杂度)

最简单的方案就是单页面 + @State 控制视图切换。ArkTS 的 @State 装饰器天然支持这种模式——当 currentTabIndex 变化时,框架自动重新渲染对应的视图分支。

@State currentTabIndex: number = 0;

build() {
  Column() {
    // 标题栏
    // Tab导航
    // 内容区域(条件渲染)
    if (this.currentTabIndex === 0) {
      this.buildFullTextView();
    } else if (this.currentTabIndex === 1) {
      this.buildReciteView();
    } else {
      this.buildQuizView();
    }
  }
}

五、数据层:诗文的数字化建模

5.1 数据结构设计

这是整个应用的基础。我需要把一篇线性文本转化为计算机可以处理的结构化数据。

核心需求:

  1. 支持分段显示(按原诗的叙事段落分组)
  2. 支持逐句背诵(需要访问每一行诗句)
  3. 支持提示模式(显示诗句的开头和结尾几个字)

基于这些需求,设计了三个数据结构:

// 一行诗句
export interface PoemLine {
  text: string;      // 完整诗句
  startHint: string; // 提示开头几个字
  endHint: string;   // 提示结尾几个字
}

// 一个段落
export interface PoemSection {
  title: string;
  lines: PoemLine[];
}

5.2 提示生成算法

提示模式的关键在于 startHintendHint。生成逻辑基于诗句长度动态调整:

function makeLine(text: string): PoemLine {
  const len = text.length;
  if (len <= 6) {
    // 短句(如"阿母谓府吏"):只显示首尾各1个字
    return {
      text,
      startHint: text.substring(0, 1),
      endHint: text.substring(len - 1),
    };
  } else if (len <= 10) {
    // 中等长度:首尾各2个字
    return {
      text,
      startHint: text.substring(0, 2),
      endHint: text.substring(len - 2),
    };
  } else {
    // 长句(含对话):首尾各3个字
    return {
      text,
      startHint: text.substring(0, 3),
      endHint: text.substring(len - 3),
    };
  }
}

这个算法的设计逻辑是:

  • 太短的句子不需要太多提示,1个字已经足够唤起记忆
  • 长句提供的提示多一个字,帮助用户定位
  • 所有长度规则在编译期决定,不在运行时计算,对性能零影响

5.3 全诗数据组织

全诗被组织为 15 个段落(含序言),每个段落按叙事节奏划分:

段落 名称 内容 行数
小序背景 3
自述 兰芝的身世陈述 11
求母 仲卿向母亲求情 5
母怒 焦母拒绝 11
话别 夫妻告别 17
严妆 兰芝梳妆辞别 18
盟誓 路口盟誓 13
还家 兰芝回家见母 8
求亲 县令求婚 15
太守 太守求婚,兄长逼迫 17
备婚 太守家备办婚礼 14
十一 悲泣 兰芝缝衣哭泣 8
十二 死别 焦仲卿赶来,生死诀别 19
十三 别母 仲卿拜别母亲 15
十四 殉情 两人殉情 7
十五 化鸟 合葬化鸟 7

段落的划分不是随意的,它遵循故事的内在叙事节奏。例如 “死别” 段是高潮,对话密集,行数最多。

5.4 按段 vs 逐句的映射

数据层同时支持两种访问模式:

// 按段访问:直接在UI层通过 POEM_SECTIONS[index] 访问
POEM_SECTIONS[this.reciteSectionIndex]

// 逐句访问:通过 getAllLines() 获取平铺数组
export function getAllLines(): PoemLine[] {
  const lines: PoemLine[] = [];
  for (const section of POEM_SECTIONS) {
    for (const line of section.lines) {
      if (line.text.trim() !== '') {
        lines.push(line);
      }
    }
  }
  return lines;
}

getAllLines() 过滤了空行(段落中的分隔留白),只返回实际有文本的诗句。这在逐句背诵和默写模式中非常重要——用户不应该在空行上浪费翻页操作。


六、页面层:三Tab框架的实现

6.1 @Component 结构

整个 Index.ets 是一个 @Component,通过 @State 装饰的变量来管理所有交互状态:

@Entry
@Component
struct Index {
  @State currentTabIndex: number = 0;     // Tab切换

  @State reciteSectionIndex: number = 0;   // 背诵-当前段
  @State reciteLineIndex: number = 0;      // 背诵-当前行
  @State showLineText: boolean = false;     // 背诵-显示/隐藏
  @State reciteMode: number = 0;           // 背诵-模式(按段/逐句)

  @State userInput: string = '';           // 默写-用户输入
  @State showAnswer: boolean = false;      // 默写-显示答案
  @State currentQuizLineIndex: number = 0; // 默写-当前行
  @State showLineResult: boolean = false;  // 默写-对错结果
}

每个 @State 变量都可以独立触发UI更新。比如当用户点击"下一句"时,只需要更新 reciteLineIndexshowLineText 两个状态,框架自动重绘对应区域。

6.2 @Builder 拆分视图

ArkTS 的 @Builder 装饰器允许将视图拆分为独立的方法,然后在 build() 中组合。这比将所有代码写在 build() 中可读性强得多:

build() {
  Column() {
    this.buildTitleBar();
    this.buildTabBar();
    if (this.currentTabIndex === 0) {
      this.buildFullTextView();
    } else if (this.currentTabIndex === 1) {
      this.buildReciteView();
    } else {
      this.buildQuizView();
    }
  }
}

6.3 自定义Tab导航

没有使用 Tab 容器组件(Tabs + TabContent),因为 ArkUI 的 Tabs 组件在早期版本中切换动画不够流畅,且自定义样式受限。我选择自己实现:

@Builder
buildTabItem(title: string, index: number) {
  Column() {
    Text(title)
      .fontSize(15)
      .fontColor(this.currentTabIndex === index ? '#E8D5B7' : '#7A7A9A')
      .fontWeight(this.currentTabIndex === index ? FontWeight.Medium : FontWeight.Regular)
    if (this.currentTabIndex === index) {
      Divider()
        .width('60%')
        .height(2)
        .color('#E8D5B7')
        .margin({ top: 4 })
    }
  }
  .height('100%')
  .justifyContent(FlexAlign.Center)
  .layoutWeight(1)
  .onClick(() => {
    this.currentTabIndex = index;
  })
}

这里的技巧:

  • .layoutWeight(1) 让三个Tab等宽平分
  • 选中项显示金色下划线,未选中不显示
  • 点击时直接修改 currentTabIndex,下方视图自动切换

七、全文浏览模式

7.1 长文本滚动的实现

ArkUI 的 Scroll + Column 组合可以处理任意长度的文本:

@Builder
buildFullTextView() {
  Scroll() {
    Column() {
      Text('孔雀东南飞')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .fontColor('#E8D5B7')
      Text('(汉乐府)')
        .fontSize(14)
        .fontColor('#A0896E')

      ForEach(POEM_SECTIONS, (section: PoemSection) => {
        Column() {
          Text(section.title)
            .fontSize(16)
            .fontColor('#C4A97D')
          ForEach(section.lines, (line: PoemLine) => {
            if (line.text === '') {
              Blank().height(12)  // 空行用 Blank 占位
            } else {
              Text(line.text)
                .fontSize(17)
                .fontColor('#D4C5A9')
                .lineHeight(30)
                .textAlign(TextAlign.Center)
                .width('100%')
            }
          })
        }
        .padding({ left: 20, right: 20 })
      })
    }
    .width('100%')
  }
}

7.2 性能考量

对于全诗 15 个段落约 170 行文本(含空行分隔),Scroll 的渲染性能完全不是问题。但如果诗句数量再大一个数量级(数千行),就需要考虑 LazyForEach 来实现虚拟列表。ArkTS 的 LazyForEach 在 API 24 中已经支持,用法与 ForEach 类似,只是需要提供一个数据源来告诉框架哪些 item 在可视区内。

7.3 样式设计

浏览模式采用了深色古风主题:

元素 颜色
背景 #1A1A2E(深蓝黑)
标题 #E8D5B7(金色)
诗句 #D4C5A9(米白)
段落标题 #C4A97D(暗金)
副标题 #A0896E(浅棕)

这套配色模仿了古籍的视觉感受——不是白底黑字的现代印刷风格,而是更接近古籍泛黄的纸张和墨色。


八、提示背诵模式

8.1 两种背诵子模式

提示背诵是应用的核心功能,支持两种模式:

  1. 按段背诵:一次显示整段内容(但被隐藏),适合把握段落结构和整体逻辑
  2. 逐句背诵:一句一句过,适合精细记忆

用户可以通过顶部的按钮自由切换:

@Builder
buildModeButton(title: string, mode: number) {
  Button(title)
    .fontColor(this.reciteMode === mode ? '#1A1A2E' : '#E8D5B7')
    .backgroundColor(this.reciteMode === mode ? '#E8D5B7' : '#3A3A5C')
    // ...
    .onClick(() => {
      this.reciteMode = mode;
      // 重置所有背诵状态
      this.reciteSectionIndex = 0;
      this.reciteLineIndex = 0;
      this.showLineText = false;
    })
}

8.2 显示/隐藏切换的逻辑

核心交互是"点击按钮切换诗句的显示状态":

Text(this.showLineText ? line.text : line.startHint + '……' + line.endHint)
  .fontColor(this.showLineText ? '#D4C5A9' : '#6A6A8A')

showLineTextfalse 时:

  • 显示 "十三……归"(三段式提示:开头 + …… + 结尾)
  • 颜色为暗灰色(#6A6A8A),视觉上不突出

showLineTexttrue 时:

  • 显示完整诗句
  • 颜色为亮色(#D4C5A9

用户可以通过同一个按钮反复切换,这与传统的"卡片翻转"式记忆法原理相同——先尝试回忆,再核对原文。

8.3 按键处理中的边界条件

背诵模式中有几个容易忽视的边界条件:

// 上一段:检查上界
.enabled(this.reciteSectionIndex > 0)
.onClick(() => {
  if (this.reciteSectionIndex > 0) {
    this.reciteSectionIndex--;
    this.showLineText = false;  // 切换段落时自动隐藏
  }
})

// 下一段:检查下界
.enabled(this.reciteSectionIndex < POEM_SECTIONS.length - 1)
  • 上界检查:第一段时"上一段"按钮禁用
  • 下界检查:最后一段时"下一段"按钮禁用
  • 自动隐藏:切换段落/句子时,showLineText 重置为 false,强迫用户在新内容上再次尝试回忆

8.4 当前进度展示

逐句背诵时显示进度条信息:

Text('第 ' + (this.reciteLineIndex + 1) + ' / ' + this.allLines.length + ' 句')
  .fontSize(14)
  .fontColor('#A0896E')

让用户随时知道自己处于背诵的哪个阶段。这对长诗的背诵至关重要——知道已经完成了多少、还剩多少,可以减轻背诵过程中的焦虑感。


九、逐句默写模式

9.1 交互流程

逐句默写是应用中最复杂的功能,交互流程如下:

  1. 系统显示提示(开头 + …… + 结尾)
  2. 用户在 TextArea 中输入诗句
  3. 点击"检查"按钮,系统自动判断对错
  4. 如果不会,可以点击"显示答案"
  5. 点击"下一句"继续

9.2 TextArea 的使用

ArkUI 的 TextArea 组件提供了多行文本输入能力:

TextArea({ text: this.userInput, placeholder: '在此输入诗句……' })
  .height(60)
  .fontSize(17)
  .fontColor('#D4C5A9')
  .placeholderFont({ size: 15, weight: FontWeight.Regular })
  .placeholderColor('#5A5A7A')
  .backgroundColor('#2A2A44')
  .borderRadius(12)
  .onChange((val: string) => {
    this.userInput = val;
    this.showLineResult = false;  // 用户修改输入时清除上次的检查结果
  })

使用要点:

  • placeholder 属性在输入框为空时显示占位提示
  • onChange 回调在每次输入时触发,用于同步状态
  • 用户修改输入时清除之前的检查结果,防止新旧结果混淆

9.3 自动判对逻辑

判断逻辑简单直接:

if (this.userInput.trim() === this.allLines[this.currentQuizLineIndex].text) {
  Text('✓ 正确!').fontColor('#7BCF8C')
} else {
  Text('✗ 再想想').fontColor('#E87A7A')
}

使用的是严格相等匹配(===)。这里有一个权衡:

  • 严格匹配:用户必须一字不差地输入,要求高,但适合精确背诵
  • 模糊匹配:如果允许部分匹配或忽略标点,可能更适合初学阶段

当前版本选择了严格模式,后续可以增加"宽松模式"选项,允许忽略中英文标点和空格的差异。

9.4 随机抽背功能

默写模式实现了"随机跳转"功能,用于测试用户对整个诗篇的掌握程度:

Button('随机跳转')
  .onClick(() => {
    const randomIdx = Math.floor(Math.random() * this.allLines.length);
    this.currentQuizLineIndex = randomIdx;
    this.userInput = '';
    this.showAnswer = false;
    this.showLineResult = false;
  })

这个功能看似简单,但实际上解决了背诵中的一个核心问题:顺序背诵容易,随机考核困难。很多人可以按顺序背出整首诗,但被打断后无法接上。随机跳转正是针对这个问题设计的。


十、编译构建与签名

10.1 构建流程

使用 hvigor 构建工具,一次完整的构建包括:

hvigor UP-TO-DATE :entry:default@PreBuild
hvigor Finished :entry:default@CompileArkTS   ← 编译 ArkTS → 方舟字节码
hvigor Finished :entry:default@CompileResource ← 编译资源
hvigor Finished :entry:default@PackageHap     ← 打包 HAP
hvigor Finished :entry:default@SignHap        ← 签名
hvigor BUILD SUCCESSFUL

整个构建流程在 8 秒左右完成(含增量编译),对于小型应用来说非常快。

10.2 签名注意事项

构建输出中有一个关键警告:

Will skip sign 'hos_hap'. No signingConfigs profile is configured in current project.

这意味着 HAP 包是未签名的。在真机上运行需要配置签名证书。调试阶段有两个选择:

  1. 自动签名(推荐):在 DevEco Studio 中登录华为开发者账号,让 IDE 自动管理签名
  2. 手动配置:在 build-profile.json5signingConfigs 中配置 keystore 信息
{
  "app": {
    "signingConfigs": [{
      "name": "default",
      "material": {
        "certpath": "xxx.cer",
        "profile": "xxx.p7b",
        "keystore": {
          "keyalias": "xxx",
          "storePassword": "xxx",
          "keypass": "xxx"
        }
      }
    }]
  }
}

10.3 部署测试

签名后的 HAP 包可以通过两种方式部署到真机或模拟器:

  1. DevEco Studio 中直接点击 Run
  2. 使用命令行:hdc install entry/build/default/outputs/default/entry-default-unsigned.hap

十一、踩坑记录与最佳实践

11.1 ForEach 的 key 生成

在 ArkTS 中,ForEach 默认使用数组索引作为 key。在遍历 POEM_SECTIONSsection.lines 时没有问题,因为数据是静态的。但如果数据是动态的(增删改),必须提供一个 keyGenerator 函数:

ForEach(arr, (item) => { /* ... */ }, (item) => item.id)

11.2 @Builder 中访问 @State

@Builder 方法中可以直接访问 @State 变量,因为 @Builder@Component 的方法,共享同一个 this 上下文。但如果在 @Builder 内部嵌套了 @Builder(或使用了 @LocalBuilder),则需要注意作用域问题。

11.3 颜色值的大小写

ArkUI 的颜色值必须使用十六进制格式,大小写敏感度较低,但建议统一使用大写:

.backgroundColor('#1A1A2E')  // ✔ 正确
.backgroundColor('#1a1a2e')  // ✔ 也正确,但建议统一

11.4 字符串模板

ArkTS 不支持 JavaScript 的模板字符串(反引号):

// ❌ 不支持
const msg = `${index + 1} / ${total}`;

// ✔ 使用字符串拼接
const msg = '第 ' + (index + 1) + ' / ' + total + ' 句';

这是一个容易踩的坑——跟 TypeScript 语法非常相似但不完全相同。

11.5 Scroll 嵌套 Column 的布局

Scroll 必须直接包裹 ColumnRow,不能直接包裹多个子元素:

// ❌ 错误
Scroll() {
  Text('a')
  Text('b')
}

// ✔ 正确
Scroll() {
  Column() {
    Text('a')
    Text('b')
  }
}

11.6 数组类型的泛型写法

ArkTS 的泛型语法要求在变量名后标注类型,而不是在构造器后:

// ✔ ArkTS 支持的写法
private allLines: PoemLine[] = getAllLines();

// ArkTS 也支持
// private allLines: Array<PoemLine> = getAllLines();

十二、总结与展望

12.1 项目数据

指标 数据
源代码文件 2 个
总代码行数 ~830 行
构建时间 ~8 秒
构建产物 HAP 约 2 MB

12.2 可以扩展的方向

这个应用目前是 MVP(最小可行产品),还有很多可以优化的方向:

  1. 语音识别背诵:利用 HarmonyOS 的语音识别能力,用户可以直接朗读诗句,系统自动判断是否正确
  2. 云端同步:接入华为帐号服务,将学习进度同步到云端
  3. AI 辅助:利用盘古大模型,对用户的背诵进行智能评分,给出针对性建议
  4. 暗黑模式:适配系统深色/浅色主题切换
  5. 更多古诗:扩展为一个通用的古诗词背诵平台
  6. 离线包:支持用户下载诗词语音包

12.3 个人感悟

用 ArkTS 构建这个项目,最大的感受是:声明式UI的威力不在于花哨的动画,而在于让状态管理变得可预测。

在传统的命令式UI中,当用户点击"下一句"时,你需要手动找到对应的 TextView,调用 setText(),再更新另一个 View 的可见性。而在 ArkTS 中,你只需要更新一个 @State 变量,剩下的交给框架去做。这种模式大幅减少了 UI 相关的 bug。

一个有趣的细节是:整个应用只用了一个 .ets 文件作为主页面。对于 830 行的代码量来说,这完全能接受。每个 @Builder 之间的边界清晰,读代码的顺序就是阅读 UI 从上到下的顺序。但如果应用继续扩展(超过 2000 行),就应该拆分为多个 Component 文件了。

12.4 写在最后

技术博客的结尾,终归要回到产品本身。

《孔雀东南飞》用它千年不衰的生命力告诉我们:真正好的作品,无论历经多少时代,总能找到与当下对话的方式。而作为开发者,能用一行行代码为经典的传播做一点微小的贡献,是一种幸运。

感谢 HarmonyOS 生态提供的技术平台,也感谢所有愿意花时间读完这篇博客的读者。如果你也在学习 ArkTS,希望这篇文章能给你一些启发。


十三、深入 State 管理与响应式编程

13.1 @State 的响应式原理

ArkTS 的 @State 装饰器基于观察者模式实现。当一个被 @State 修饰的变量发生变化时,框架会自动标记依赖该变量的组件为"脏"(dirty),并在下一帧进行重绘。开发者不需要手动调用 setState()notifyDataSetChanged()——这些都是框架自动完成的。

在我们的应用中,每个 @State 变量的变化都会触发不同范围的UI更新:

@State 变量 变化时影响的范围 更新复杂度
currentTabIndex 整个内容区域 全量重建
reciteLineIndex 仅当前行Text 局部更新
showLineText 当前段/行的Text节点 局部更新
userInput TextArea + 结果区域 局部更新
showLineResult 仅对错提示 局部更新

框架的渲染引擎足够智能,只会重新渲染依赖于变化状态的 if 分支或 Text 节点,而不是重建整个组件树。这意味着在 Tab 切换时(currentTabIndex 变化),只有对应分支的 @Builder 会被执行,其他两个 Tab 的内容会被跳过。

13.2 @Prop 与 @Link 的使用时机

虽然这个应用没有使用子组件(所有视图都集中在 Index.ets 中),但如果需要将视图拆分为独立组件,就需要理解 @Prop@Link 的区别:

  • @Prop:单向数据流。父组件传递给子组件的数据,子组件可以读取但不能修改(修改会触发编译错误或运行时警告)
  • @Link:双向绑定。父组件和子组件共享同一个状态,任一方修改都会同步到另一方
// 父组件
@Component
struct Parent {
  @State count: number = 0;
  build() {
    Child({ count: this.count })           // @Prop: 只读传递
    LinkedChild({ count: this.count })     // @Link: 双向绑定
  }
}

// 子组件 - 只读
@Component
struct Child {
  @Prop count: number;
  build() { Text('' + this.count) }
}

// 子组件 - 双向
@Component
struct LinkedChild {
  @Link count: number;
  build() { Button('+').onClick(() => { this.count++ }) }
}

在这个应用中,如果未来将三个 Tab 拆分为独立的 @Component,背诵 Tab 的 showLineText 应该用 @Link 传递(因为背诵组件内部会修改它),而全诗数据 POEM_SECTIONS 则适合用 @Prop 传递(只读)。

13.3 @Watch 的使用场景

@Watch 装饰器可以在 @State 变量变化时执行一个回调函数。这在需要"监听变化后执行副作用"的场景中非常有用:

@State @Watch('onLineChange') reciteLineIndex: number = 0;

onLineChange(): void {
  // 每次切换诗句时自动重置状态
  this.showLineText = false;
  this.showAnswer = false;
  // 可以在这里添加日志或统计分析
  console.info(`用户浏览到第 ${this.reciteLineIndex + 1}`);
}

在这个应用中,我没有使用 @Watch,因为所有状态重置逻辑都放在按钮的 onClick 回调中手动处理。但如果状态管理变得更复杂,@Watch 可以确保某个状态变化时的一定执行某个逻辑,比手动调用更可靠。


十四、Stage 模型的生命周期管理

14.1 UIAbility 的生命周期

我们的应用逻辑全部在 Index.ets@Component 中,但 Ability 层的生命周期管理仍然值得了解。EntryAbility.ets 中定义了应用级别的生命周期:

export default class EntryAbility extends UIAbility {
  onCreate(want, launchParam) { /* 应用启动时调用 */ }
  onDestroy() { /* 应用销毁时调用 */ }
  onWindowStageCreate(windowStage) {
    windowStage.loadContent('pages/Index', (err) => {
      // 在这里加载主页面
    });
  }
  onForeground() { /* 应用进入前台 */ }
  onBackground() { /* 应用进入后台 */ }
}

对于我们的背诗应用,onBackgroundonForeground 可以用于实现"进度持久化":

import { localStorage } from '@kit.ArkUI';

onBackground(): void {
  // 保存当前背诵进度
  AppStorage.set<number>('lastSection', this.reciteSectionIndex);
  AppStorage.set<number>('lastLine', this.reciteLineIndex);
}

onForeground(): void {
  // 恢复上次的背诵进度
  const savedSection = AppStorage.get<number>('lastSection') ?? 0;
  const savedLine = AppStorage.get<number>('lastLine') ?? 0;
  this.reciteSectionIndex = savedSection;
  this.reciteLineIndex = savedLine;
}

14.2 AppStorage 与 LocalStorage

ArkTS 提供了两个用于跨组件共享状态的存储方案:

  • AppStorage:应用级别的全局单例,所有组件都可以访问。适合保存用户设置、学习进度等跨页面数据
  • LocalStorage:页面级别的共享存储,在 Ability 和 Page 之间共享
// AppStorage 使用示例
AppStorage.setOrCreate('fontSize', 17);  // 设置默认值

// 在组件中绑定
@StorageLink('fontSize') fontSize: number = 17;

// 在另一个组件中读取
@StorageProp('fontSize') fontSize: number = 17;

14.3 配置持久化方案

为了给用户更好的体验,我设计了以下持久化方案(作为未来迭代方向):

AppStorage 键值规划:
├── 'fontSize'       → 用户偏好的字体大小 (默认 17)
├── 'lastSection'    → 上次背诵的段落索引
├── 'lastLine'       → 上次背诵的诗句索引
├── 'reciteMode'     → 上次使用的背诵模式 (0/1)
├── 'favoriteLines'  → 用户收藏的诗句索引数组
└── 'completedLines' → 已完成的诗句索引 Set

这些数据通过 AppStorage 实现跨会话的持久化,即使用户关闭应用再打开,进度也不会丢失。


十五、格式化与视觉设计细节

15.1 字体选择与排版

ArkUI 中的 Text 组件支持丰富的排版属性。在古诗应用中,以下几个属性特别重要:

Text('孔雀东南飞')
  .fontSize(24)           // 字体大小
  .fontWeight(FontWeight.Bold)  // 字重
  .fontColor('#E8D5B7')   // 字体颜色
  .lineHeight(30)         // 行高(影响阅读体验的关键)
  .textAlign(TextAlign.Center) // 居中对齐(古诗的美学需求)
  .letterSpacing(2)       // 字符间距(古风效果)

行高(lineHeight)对古诗阅读体验的影响最大。古诗每行字数少,如果行高太小,阅读时容易跳行;如果太大,又浪费空间。经过测试,对于 17px 的字体大小,30px 的行高是最舒适的。

15.2 颜色体系设计

应用的颜色系统基于深色古风主题,采用了色彩映射表来保证一致性:

CSS 变量名 色值 用途
bg-primary #1A1A2E 主背景
bg-secondary #16213E 标题栏背景
bg-card #2A2A44 输入框/卡片背景
tab-inactive #3A3A5C Tab/按钮背景(未选中)
text-gold #E8D5B7 标题/选中Tab文字
text-body #D4C5A9 诗句正文
text-section #C4A97D 段落标题
text-muted #A0896E 副标题/提示文字
text-hint #6A6A8A 隐藏状态的诗句
text-placeholder #5A5A7A 输入框占位符
success #7BCF8C 正确提示
error #E87A7A 错误提示

所有的颜色值都直接硬编码在 Index.ets 中。对于更大型的应用,建议将这些颜色值提取到 resources/base/element/color.json 中,通过 $r('app.color.xxx') 引用,这样可以支持系统级的暗黑/浅色主题适配。

15.3 响应式布局考量

虽然目前应用只适配了竖屏手机,但在设计时已经考虑了几个响应式的关键点:

.layoutWeight(1)   // 等分空间
.width('100%')     // 宽度自适应
.height('100%')    // 高度自适应
.padding({ left: 20, right: 20 })  // 安全的边距

.layoutWeight(1) 是 ArkUI 中最强大的布局工具之一。它类似于 Flexbox 中的 flex: 1,可以让多个子元素按权重等分父容器空间。如下所示:

Row() {
  Button('检查').layoutWeight(1)
  Button('显示答案').layoutWeight(1)
  Button('下一句').layoutWeight(1)
}

三个按钮自动等宽,无论屏幕宽度如何变化,始终保持三等分。


十六、测试与质量保障

16.1 ArkTS 单元测试

HarmonyOS 提供了 @ohos/hypium 测试框架,支持标准的三段式测试(Arrange-Act-Assert):

// 在 ohosTest 模块中
import { describe, it, expect } from '@ohos/hypium';
import { getAllLines, POEM_SECTIONS } from '@/models/PoemData';

export default function poemDataTest() {
  describe('PoemDataTest', () => {
    it('should have 15 sections', 0, () => {
      expect(POEM_SECTIONS.length).assertEqual(15);
    });

    it('should have non-empty lines', 0, () => {
      const lines = getAllLines();
      expect(lines.length).assertGreaterThan(0);
      lines.forEach(line => {
        expect(line.text.length).assertGreaterThan(0);
        expect(line.startHint.length).assertGreaterThan(0);
        expect(line.endHint.length).assertGreaterThan(0);
      });
    });

    it('startHint should be prefix of text', 0, () => {
      const lines = getAllLines();
      lines.forEach(line => {
        expect(line.text.startsWith(line.startHint)).assertTrue();
        expect(line.text.endsWith(line.endHint)).assertTrue();
      });
    });

    it('should contain first line', 0, () => {
      const lines = getAllLines();
      expect(lines[0].text).assertEqual('孔雀东南飞,五里一徘徊。');
    });
  });
}

这些测试覆盖了数据层最核心的契约:

  • 段落数量固定为 15
  • 所有诗句非空
  • 提示内容必须是完整诗句的前缀/后缀
  • 第一行诗句必须正确

16.2 本地测试执行

在 DevEco Studio 中执行本地测试(非模拟器):

hvigorw run ohosTest --module entry --product default --build-mode debug
// entry/oh-package.json5 中的测试配置
{
  "devDependencies": {
    "@ohos/hypium": "1.0.25",
    "@ohos/hamock": "1.0.0"
  }
}

16.3 UI 交互测试

对于 UI 层,可以利用 @ohos/hamock 进行组件级别的交互测试。例如测试"下一句"按钮是否正常工作:

import { renderComponent } from '@ohos/hamock';

// 模拟点击下一句按钮
const component = renderComponent(Index);
const nextButton = component.findComponent('nextLineButton');
nextButton.triggerClick();

// 验证状态变化
expect(component.getState('reciteLineIndex')).assertEqual(1);
expect(component.getState('showLineText')).assertFalse();

16.4 性能测试指标

对于古诗背诵这类文本密集型应用,需要关注以下性能指标:

指标 目标值 测试方法
首屏渲染时间 < 500ms performance.now() 打点
Scroll 帧率 > 55fps DevEco Profiler
内存占用 < 80MB DevEco Profiler
HAP 包体积 < 3MB 查看构建产物
状态更新延迟 < 16ms Profiler 帧分析

对于当前应用,这些指标都很容易达成,因为:

  • 数据量小(不到 200 条文本节点)
  • 无网络请求
  • 无图片加载
  • 无动画循环

十七、API 24 (SDK 6.1.1) 新特性利用

17.1 API 24 的关键变化

HarmonyOS SDK 6.1.1 (API 24) 引入了多项对开发者友好的改进:

  1. 方舟编译器性能优化:@Component 的编译产物更小,运行时内存占用更低
  2. @Builder 参数传递增强:支持在 @Builder 方法中传递回调函数
  3. Scroll 组件改进:支持 edgeEffect 属性,可以控制滚动到边界时的回弹效果
  4. TextArea 增强:placeholder 属性支持单独设置字体颜色和大小

17.2 项目中实际使用的 API 24 特性

在我们的应用中,以下特性依赖 API 24:

// 1. TextArea 的 placeholderFont 单独设置
TextArea({ placeholder: '在此输入诗句……' })
  .placeholderFont({ size: 15, weight: FontWeight.Regular })
  .placeholderColor('#5A5A7A')
// 这在 API 23 及之前版本中是不支持的

// 2. Scroll 的 edgeEffect(如果使用的话)
Scroll() { /* ... */ }
  .edgeEffect(EdgeEffect.None) // 不显示滚动到边界的效果
  // API 24 新增

// 3. Divider 的颜色和宽度自定义
Divider()
  .width('60%')
  .height(2)
  .color('#E8D5B7')

17.3 Stage 模型的强制使用

API 24 强制使用 Stage 模型,这意味着:

  • 不再支持 FA(Feature Ability)模型的 @Entry + @Page 组合
  • 必须在 module.json5 中显式声明 "srcEntry": "./ets/entryability/EntryAbility.ets"
  • Ability 的生命周期方法与 Stage 模型的规范严格绑定
{
  "abilities": [{
    "name": "EntryAbility",
    "srcEntry": "./ets/entryability/EntryAbility.ets",
    "launchType": "singleton"  // 默认单实例模式
  }]
}

十八、项目复盘与经验总结

18.1 开发时间线

阶段 耗时 主要工作
数据建模 30分钟 定义 PoemLine/PoemSection 接口,录入全诗
主框架搭建 20分钟 三Tab导航 + 标题栏
全文浏览 10分钟 Scroll + ForEach 遍历
提示背诵 25分钟 两种模式 + 显示/隐藏逻辑
逐句默写 20分钟 TextArea + 输入检查 + 随机跳转
调试优化 15分钟 边界条件修复 + 视觉调整
总计 ~2小时

18.2 代码量统计

PoemData.ets:  332 行(含数据与类型定义)
Index.ets:     502 行(含 UI 与逻辑)
Total:         834 行

其中数据(诗文本身)占据了将近一半的篇幅,纯逻辑代码(UI + 交互)约 500 行。

18.3 值得保持的好习惯

  1. 数据与视图分离PoemData.ets 零 UI 依赖,可以在控制台直接测试
  2. 边界条件前置:按钮的 .enabled() 条件在绑定数据时就确定,不会出现越界
  3. 状态重置的一致性:每次导航(上一句/下一句/随机跳转)都重置相关的 UI 状态
  4. 颜色编码系统化:使用统一的色板,避免"五彩斑斓的黑"

18.4 可以改进的地方

如果重新做这个项目,我会在以下方面做得更好:

  1. 使用 LazyForEach 替代 ForEach,为扩展到支持更多古诗做准备
  2. 拆分子组件,将每个 Tab 的 @Builder 提取为独立的 @Component,以便单独测试
  3. 增加 Accessibility 支持:为 Text 组件添加 accessibilityText,方便视障用户使用屏幕阅读器
  4. 边距使用 $r 资源引用替代硬编码数字,方便适配不同屏幕密度

附录A:完整项目文件清单

MyApplication/
├── AppScope/
│   └── app.json5                    # 应用元信息
├── entry/
│   ├── src/main/
│   │   ├── ets/
│   │   │   ├── entryability/
│   │   │   │   └── EntryAbility.ets  # Ability 生命周期
│   │   │   ├── models/
│   │   │   │   └── PoemData.ets      # 诗词数据模型(手动创建)
│   │   │   └── pages/
│   │   │       └── Index.ets         # 主页面(手动创建)
│   │   ├── module.json5              # 模块配置
│   │   └── resources/                # 资源文件
│   └── build-profile.json5           # 模块构建配置
├── build-profile.json5               # 项目构建配置
├── hvigor/hvigor-config.json5        # hvigor 构建工具配置
└── oh-package.json5                  # 项目依赖声明

附录B:全诗结构速查表

段落 名称 起止诗句 行数 故事节点
小序 3 背景交代
自述 孔雀东南飞…及时相遣归 11 矛盾爆发
求母 府吏得闻之…何意致不厚 5 仲卿求情
母怒 阿母谓府吏…会不相从许 11 焦母拒绝
话别 府吏默无声…久久莫相忘 17 夫妻告别
严妆 鸡鸣外欲曙…涕落百余行 18 辞别上路
盟誓 府吏马在前…二情同依依 13 路口盟誓
还家 入门上家堂…阿母大悲摧 8 回家见母
求亲 还家十余日…不得便相许 15 县令求亲
太守 媒人去数日…便可作婚姻 17 太守逼婚
备婚 媒人下床去…郁郁登郡门 14 太守备婚
十一 悲泣 阿母谓阿女…愁思出门啼 8 兰芝悲泣
十二 死别 府吏闻此变…千万不复全 19 生死诀别
十三 别母 府吏还家去…渐见愁煎迫 15 仲卿别母
十四 殉情 其日牛马嘶…自挂东南枝 7 双双殉情
十五 化鸟 两家求合葬…戒之慎勿忘 7 化鸟升华

本文涉及的完整源码可在 DevEco Studio 项目中查看。构建环境:HarmonyOS API 24 (SDK 6.1.1),DevEco Studio 6.1.x。

Logo

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

更多推荐