ArkTS 口语陪练:基于 HarmonyOS 的 21 天语言训练营开发全流程详解

本文将以一个完整的 HarmonyOS 应用 ——「21 天语言训练营」为例,从项目背景、技术选型、目录结构、核心功能实现、UI 设计、关键技术细节到性能优化,进行一次系统化的深度拆解。读完之后,你不仅能复刻本项目,还能掌握 ArkTS 在 UI 构建、状态管理、路由导航、列表与网格控件上的全部最佳实践。文章末尾还附带了完整的扩展方向与未来演进路线图,帮助你把项目从"能跑"打磨到"能用"再到"能商业化"。

运行截图:

在这里插入图片描述

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


一、引言:为什么要在 HarmonyOS 上做口语陪练

随着 HarmonyOS 5 / 6 的全面铺开,鸿蒙生态已经走过了"能不能用"的阶段,进入了"好不好用"的阶段。越来越多的开发者开始把目光从 Android 与 iOS 转向 HarmonyOS ArkUI(ArkTS)开发。但在实际开发中,初学者往往会被几个共性问题卡住:ArkTS 到底和 Flutter、SwiftUI、Jetpack Compose 有什么不同?Grid、List 这些"老朋友"在 ArkUI 里要怎么写才对?状态管理怎么做?为什么我用 @State 修饰的变量明明赋值了但界面不刷新?页面之间怎么跳转、数据怎么传、参数怎么回传?

本项目「21 天语言训练营」正是为了解决这些疑问而生的。它只用最朴素的需求(语言学习)、最常见的控件(Grid 与 List)、最基础的 API(@State、@Builder、router),就完成了一个可以真机运行、商业级别的口语陪练 App。整个 App 的核心场景非常清晰:第一,闪卡背诵(Grid)。用两列网格展示短语与单词闪卡,点击翻转查看翻译、音标、例句。第二,AI 对话记录(List)。用列表展示历史 AI 对话练习的结果,包括话题、角色、时长、得分。第三,学习概览(顶部数据看板)。展示已学单词、练习次数、连续打卡天数等关键指标。

麻雀虽小,五脏俱全。下面我们就一步步拆解它,让你从"看得懂"到"写得出来"再到"写得漂亮"。


二、项目背景与产品定位

2.1 业务背景

在英语学习中,"短时高频 + 间隔重复"是被验证最有效的记忆方式。把这个理论映射到产品形态上,就是闪卡(Flashcard)系统。同时,"真实场景对话"是语言学习中提升流利度最高效的手段,但真人外教成本太高,AI 陪练成了最优解。因此,我们的产品形态是「闪卡打基础 + AI 对话做实战」。前者负责知识的输入与巩固,后者负责能力的输出与检验。

从用户旅程上看,一个典型的学习闭环是这样的:早晨醒来,打开 App → 复习 10 张闪卡(Grid)→ 完成 1 轮 AI 口语对话(List 中新增一条记录)→ 看到自己的得分与连续打卡天数增加 → 获得成就感 → 第二天继续。21 天后,用户养成习惯,付费意愿随之产生。

2.2 项目目标

技术上,本项目完整演示 ArkTS 中 Grid、List、@State、@Builder、router 等核心 API 的正确用法,让初学者看完就能复刻。体验上,界面简洁现代,操作流畅,闪卡翻转有即时反馈,对话记录一目了然。可扩展上,数据模型独立、UI 与逻辑解耦,方便后续接入真实 AI 接口、支付系统、用户体系。

2.3 目标用户

本项目的目标用户有三类。第一类,鸿蒙初学者:想找一个完整、可跑、可学的开源 ArkTS 项目作为入门参照。第二类,语言学习产品经理:想看一个可落地的原型,验证商业模式与转化漏斗。第三类,转型开发者:以前做 Android / iOS / 前端,想快速理解 ArkUI 的声明式思维模型。

2.4 为什么不选跨平台方案

你可能会问:既然 Flutter、React Native 都能跨端,为什么还要用 ArkTS?答案很简单:原生体验、性能优势、鸿蒙生态绑定。ArkTS 是 HarmonyOS 的"一等公民",它能直接调用所有鸿蒙原生能力,比如 CoreSpeechKit 语音识别、AI 引擎、原子化服务、跨设备流转等。这些能力用跨平台框架调用,要么绕弯子,要么根本调不到。从长期来看,鸿蒙生态的开发者红利只会越来越大,提前布局 ArkTS 是非常划算的。


三、技术选型与开发环境

3.1 技术栈

维度 选型 原因
框架 ArkUI(ArkTS) HarmonyOS 官方 UI 框架,声明式
语言 ArkTS TypeScript 超集,鸿蒙原生支持
IDE DevEco Studio 官方 IDE,配套模拟器与真机调试
SDK 6.1.1(24) 与 build-profile.json5 对齐
目标设备 phone 模块 deviceTypes 中声明
路由 @kit.ArkUI 中的 router 官方页面跳转能力
状态管理 @State 简单场景足够,避免引入过度设计

3.2 为什么选 ArkTS 而不是其他

ArkTS 的优势可以归纳为四点。第一,声明式。和 SwiftUI、Flutter、Jetpack Compose 一脉相承,学习成本低,思维模型一致。第二,强类型。TypeScript 基础,编译期就能发现大量错误,IDE 智能提示完善。第三,生态完整。官方控件库丰富,文档详尽,社区成长快。第四,性能好。基于方舟编译器,运行效率优于大多数跨平台方案。

3.3 开发环境准备

第一步,下载 DevEco Studio。建议使用 5.0 以上版本,对应 HarmonyOS 5 与 6。第二步,安装 SDK。在 DevEco Studio 中通过 SDK Manager 安装 6.1.1(24) 版本的 API。第三步,准备模拟器或真机。模拟器适合前期快速调试,真机适合后期验证性能与体验。第四步,Node.js。DevEco Studio 自带 Node 运行时,无需额外配置。

3.4 项目配置文件说明

build-profile.json5 关键字段定义了产品名 default、目标 SDK 6.1.1(24)、兼容 SDK 6.1.1(24)、运行时 HarmonyOS、构建模式 debug 与 release。

oh-package.json5(项目根)的 modelVersion 是 6.1.1,devDependencies 包含 @ohos/hypium 1.0.25(用于单元测试)和 @ohos/hamock 1.0.0(用于 mock 数据)。

entry/oh-package.json5(模块)保持空依赖,所有 UI 能力来自 ArkUI 内置包,无需引入第三方依赖。


四、项目目录结构解析

整个项目遵循 HarmonyOS 标准的多模块结构。AppScope 目录存放应用级配置,包括 app.json5 和 resources 中的应用图标、启动图。entry 是主入口模块,包含 src/main/ets/entryability(入口 Ability)、entrybackupability(备份能力)、pages(页面文件)。src/main/module.json5 定义模块清单,src/main/resources 存放颜色、字符串、布局资源。

重点关注的几个目录与文件。entry/src/main/ets/pages/ 是所有页面文件,ArkTS 的页面必须以 .ets 结尾。entry/src/main/resources/base/profile/main_pages.json 注册所有可路由的页面,没有注册的页面无法通过 router 跳转。entry/src/main/module.json5 模块清单,定义入口 Ability、支持设备类型、应用图标等。AppScope/resources/base/element/string.json 应用名、应用描述等字符串资源。

这种结构的最大好处是关注点分离。Ability 负责生命周期,pages 负责 UI,resources 负责静态资源,profile 负责路由配置。即使项目规模扩大到几十个页面,结构依然清晰。


五、核心功能实现详解

5.1 入口页与路由跳转

入口页 Index.ets 是一个简洁的欢迎页,包含一个"进入语言学习"按钮,点击后跳转到主功能页 LanguageLearning.ets。完整代码如下。

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

@Entry
@Component
struct Index {
  @State message: string = 'Hello World';

  build() {
    Column() {
      Text('🌐 21 天语言训练营')
        .fontSize(28)
        .fontWeight(FontWeight.Bold)
        .fontColor('#1976D2')
        .margin({ top: 80, bottom: 16 })

      Text(this.message)
        .id('HelloWorld')
        .fontSize($r('app.float.page_text_font_size'))
        .fontWeight(FontWeight.Bold)
        .margin({ bottom: 40 })
        .onClick(() => {
          this.message = 'Welcome';
        })

      Text('系统化训练 · 闪卡背诵 · AI 对话')
        .fontSize(14)
        .fontColor('#666666')
        .margin({ bottom: 40 })

      Button('进入语言学习')
        .width('70%')
        .height(48)
        .fontSize(16)
        .backgroundColor('#1976D2')
        .onClick(() => {
          router.pushUrl({ url: 'pages/LanguageLearning' });
        })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F7FA')
  }
}

关键点解析。第一,@Entry 与 @Component 装饰器。@Entry 标记这是页面的入口,@Component 标记这是一个 UI 组件,二者缺一不可。第二,import router。从 @kit.ArkUI 中导入 router 模块,这是 ArkTS 官方提供的页面跳转能力。第三,router.pushUrl。跳转到 LanguageLearning 页面,url 是相对于 pages 目录的路径。第四,$r(‘app.float.xxx’)。通过资源 ID 引用资源文件中的常量,这是 ArkTS 推荐的做法。

main_pages.json 关键配置。

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

这里有个新手常踩的坑:忘记在 main_pages.json 中注册目标页面,导致 router.pushUrl 失败并报"url is not registered"错误。解决办法就是老老实实把所有页面都登记进来。

5.2 Grid 闪卡模块完整实现

闪卡模块是本项目的核心亮点之一,使用 ArkTS 的 Grid 加 GridItem 加 ForEach 实现两列网格布局,每张卡片支持点击翻转。

5.2.1 状态设计
@State flashcards: FlashCard[] = [
  new FlashCard(1, 'Hello', '你好', '/həˈloʊ/', 'Hello, how are you?'),
  new FlashCard(2, 'Thank you', '谢谢', '/θæŋk juː/', 'Thank you for your help.'),
  new FlashCard(3, 'Goodbye', '再见', '/ɡʊdˈbaɪ/', 'Goodbye, see you tomorrow.'),
  new FlashCard(4, 'Please', '请', '/pliːz/', 'Could you help me, please?'),
  new FlashCard(5, 'Sorry', '对不起', '/ˈsɒri/', 'I am sorry for being late.'),
  new FlashCard(6, 'Apple', '苹果', '/ˈæp.əl/', 'I eat an apple every day.'),
];

@State flippedIds: number[] = [];
@State selectedTab: number = 0;

关键点解析。flippedIds 用 number[] 而不是 Set:ArkTS 中 @State 对 Set 的响应式支持不完善,concat 与 filter 返回新数组的方式更可靠。selectedTab 控制当前显示哪个 Tab 页签,0 表示闪卡,1 表示对话记录。isFlipped(id) 辅助方法判断某张卡片是否已翻转。

isFlipped(id: number): boolean {
  return this.flippedIds.indexOf(id) >= 0;
}
5.2.2 Grid 布局完整实现
@Builder
FlashCardGrid() {
  Grid() {
    ForEach(this.flashcards, (item: FlashCard) => {
      GridItem() {
        Column() {
          if (this.isFlipped(item.id)) {
            // 背面:显示释义和例句
            Text(item.translation)
              .fontSize(20)
              .fontWeight(FontWeight.Bold)
              .fontColor('#1976D2')
            Text(item.phonetic)
              .fontSize(13)
              .fontColor('#888888')
              .margin({ top: 6 })
            Text(item.example)
              .fontSize(12)
              .fontColor('#555555')
              .margin({ top: 10 })
              .maxLines(3)
              .textOverflow({ overflow: TextOverflow.Ellipsis })
          } else {
            // 正面:显示单词
            Text(item.word)
              .fontSize(22)
              .fontWeight(FontWeight.Bold)
              .fontColor('#333333')
            Text('点击查看释义')
              .fontSize(12)
              .fontColor('#AAAAAA')
              .margin({ top: 10 })
          }
        }
        .width('100%')
        .height(140)
        .padding(14)
        .backgroundColor('#FFFFFF')
        .borderRadius(12)
        .justifyContent(FlexAlign.Center)
        .alignItems(HorizontalAlign.Center)
        .onClick(() => {
          if (this.isFlipped(item.id)) {
            this.flippedIds = this.flippedIds.filter((v: number) => v !== item.id);
          } else {
            this.flippedIds = this.flippedIds.concat([item.id]);
          }
        })
      }
    }, (item: FlashCard) => item.id.toString())
  }
  .columnsTemplate('1fr 1fr')
  .columnsGap(12)
  .rowsGap(12)
  .padding(16)
  .layoutWeight(1)
}

关键点解析。@Builder 装饰器把 UI 片段抽成可复用函数,FlashCardGrid() 是实例方法而非全局函数,所以需要通过 this.FlashCardGrid() 调用。columnsTemplate(‘1fr 1fr’) 定义两列等宽网格,fr 是 fraction 单位的简写,表示"等分"。columnsGap(12) 与 rowsGap(12) 设置列间距和行间距,让卡片之间有合适的呼吸感。ForEach 的第二个参数是 key 生成器,第三个参数 (item: FlashCard) => item.id.toString() 必须提供且唯一,用于优化列表渲染。

点击翻转时用 filter 与 concat 返回新数组,这是 ArkTS 触发 @State 响应式刷新的标准做法。如果直接调用 splice 或 push 修改原数组,UI 不会更新,这是 ArkTS 初学者最容易踩的坑之一。

5.3 List 对话记录模块完整实现

List 模块展示 AI 对话练习的历史记录,每条记录包含话题、角色、时长、得分等信息。

5.3.1 数据结构定义
class ConversationRecord {
  id: number = 0;
  topic: string = '';
  partner: string = '';
  duration: string = '';
  score: number = 0;
  timeText: string = '';
  status: string = '';

  constructor(id: number, topic: string, partner: string, duration: string, score: number, timeText: string, status: string) {
    this.id = id;
    this.topic = topic;
    this.partner = partner;
    this.duration = duration;
    this.score = score;
    this.timeText = timeText;
    this.status = status;
  }
}

字段说明:id 是唯一标识,用于 ForEach 的 key 生成;topic 是对话主题(如"餐厅点餐");partner 是 AI 角色名称(如"AI 店员 Emma");duration 是对话时长(如"08:32");score 是 AI 给出的评分(0 到 100);timeText 是相对时间描述(如"今天 09:15");status 是等级描述(如"优秀")。

5.3.2 List 布局完整实现
@Builder
ConversationList() {
  List() {
    ForEach(this.conversations, (item: ConversationRecord) => {
      ListItem() {
        Row() {
          // 左侧头像圆
          Column() {
            Text(item.partner.substring(item.partner.length - 1))
              .fontSize(20)
              .fontColor('#FFFFFF')
              .fontWeight(FontWeight.Bold)
          }
          .width(48)
          .height(48)
          .borderRadius(24)
          .backgroundColor(this.StatusColor(item.score))
          .justifyContent(FlexAlign.Center)
          .alignItems(HorizontalAlign.Center)

          // 中间信息
          Column() {
            Text(item.topic)
              .fontSize(16)
              .fontWeight(FontWeight.Bold)
              .fontColor('#333333')
            Row() {
              Text(item.partner)
                .fontSize(12)
                .fontColor('#888888')
              Text(' · ')
                .fontSize(12)
                .fontColor('#CCCCCC')
              Text(item.duration)
                .fontSize(12)
                .fontColor('#888888')
            }
            .margin({ top: 4 })

            Text(item.timeText)
              .fontSize(11)
              .fontColor('#AAAAAA')
              .margin({ top: 4 })
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Start)
          .margin({ left: 12 })

          // 右侧得分
          Column() {
            Text(item.score.toString())
              .fontSize(20)
              .fontWeight(FontWeight.Bold)
              .fontColor(this.StatusColor(item.score))
            Text(item.status)
              .fontSize(11)
              .fontColor('#888888')
              .margin({ top: 2 })
          }
          .alignItems(HorizontalAlign.End)
        }
        .width('100%')
        .padding(14)
        .backgroundColor('#FFFFFF')
        .borderRadius(10)
      }
      .padding({ left: 16, right: 16, top: 6, bottom: 6 })
    }, (item: ConversationRecord) => item.id.toString())
  }
  .layoutWeight(1)
  .backgroundColor('#F5F7FA')
  .listDirection(Axis.Vertical)
  .divider({ strokeWidth: 0 })
}

关键点解析。List 加 ListItem 加 ForEach 是 ArkTS 中列表渲染的标准三件套。列表项内部用 Row 加三个 Column 实现"左头像加中信息加右得分"的经典布局。layoutWeight(1) 让中间列占据剩余空间,实现自适应布局。divider({ strokeWidth: 0 }) 隐藏默认分割线,因为我们用了 borderRadius 的卡片样式。listDirection(Axis.Vertical) 明确指定列表方向(默认值就是垂直,但显式声明更清晰)。

5.4 Tab 切换的两种实现

页面顶部有两个 Tab:“短语闪卡"和"AI 对话记录”,通过 @State selectedTab 控制显示内容。本项目用的是 if 条件渲染的方式,简单直接。

Row() {
  Text('短语闪卡')
    .layoutWeight(1)
    .fontSize(15)
    .fontWeight(this.selectedTab === 0 ? FontWeight.Bold : FontWeight.Normal)
    .fontColor(this.selectedTab === 0 ? '#1976D2' : '#666666')
    .padding(10)
    .onClick(() => {
      this.selectedTab = 0;
    })
  Text('AI 对话记录')
    .layoutWeight(1)
    .fontSize(15)
    .fontWeight(this.selectedTab === 1 ? FontWeight.Bold : FontWeight.Normal)
    .fontColor(this.selectedTab === 1 ? '#1976D2' : '#666666')
    .padding(10)
    .onClick(() => {
      this.selectedTab = 1;
    })
}
.width('100%')
.border({ width: { bottom: 1 }, color: '#EEEEEE' })

// 内容区
if (this.selectedTab === 0) {
  this.FlashCardGrid()
} else {
  this.ConversationList()
}

关键点解析。通过三元运算符动态改变文字的 fontWeight 和 fontColor,实现简单的 Tab 高亮效果。用 if 语句控制内容区显示,比 visibility 属性更彻底(不会占用布局空间)。如果想做成滑动切换效果,可以用 Tabs 组件,但本项目用 if 切换更轻量,代码也更易读。

ArkTS 也提供了原生的 Tabs 组件,它支持顶部导航、侧边导航、底部导航三种样式,还能配合 TabContent 实现滑动切换。如果你的 Tab 数量较多(≥3 个)或者需要更丰富的交互动画,建议升级到 Tabs 组件。


六、数据模型设计最佳实践

6.1 FlashCard 模型

class FlashCard {
  id: number = 0;
  word: string = '';
  translation: string = '';
  phonetic: string = '';
  example: string = '';
  mastered: boolean = false;

  constructor(id: number, word: string, translation: string, phonetic: string, example: string) {
    this.id = id;
    this.word = word;
    this.translation = translation;
    this.phonetic = phonetic;
    this.example = example;
  }
}

字段说明:id 是唯一标识,用于 ForEach 的 key 生成;word 是单词或短语本体(英文);translation 是中文翻译;phonetic 是音标(IPA);example 是例句,展示单词在真实语境中的用法;mastered 是是否已掌握(预留字段,后续可接入间隔重复算法)。

6.2 ConversationRecord 模型

该模型在 5.3.1 节已经详述,这里不再重复。

6.3 模型设计的四条最佳实践

第一,类比接口更实用。在 ArkTS 中,类比接口更适合做数据模型,因为类可以有默认值和构造函数,UI 中可以直接 new 使用,无需额外写工厂函数。第二,字段默认值。所有字段都给出默认值(如 id: number = 0),避免 ArkTS 严格模式下的空值检查报错。第三,构造器封装。用 constructor 封装字段初始化逻辑,调用方更简洁。第四,预留扩展点。mastered 字段就是预留的,后续接入 SM-2 或 Anki 算法时直接用,不需要再改模型。

为什么不用 interface?在 ArkTS 严格模式下,interface 不能作为运行时类型,实例化时无法保证字段被正确赋值。而 class 既是类型也是值,编译器会强制要求所有字段被初始化,运行时也支持 instanceof 检查,更适合业务开发。


七、UI 设计思路与色彩系统

7.1 色彩系统

本项目采用了一套简洁的三色系统。主色 #1976D2 用于标题、按钮、强调文字,是 Material Design 蓝的微调版本,稳重而不失活力。成功色 #4CAF50 用于高分(≥90)以及"已学单词"指标,传递积极信号。信息色 #2196F3 用于中高分(80 到 89)以及"练习次数"指标,传达中性信息。警告色 #FF9800 用于中分(60 到 79)以及"连续打卡"指标,提示需要加强。危险色 #F44336 用于低分(小于 60),警示用户需要重新练习。背景色 #F5F7FA 作为页面底色,比纯白柔和。卡片色 #FFFFFF 作为闪卡、列表项背景。次要文字 #888888 用于辅助信息。弱化文字 #AAAAAA 用于占位提示。

7.2 排版规范

标题字号 22 到 28pt,FontWeight.Bold。正文字号 14 到 16pt,FontWeight.Normal。辅助文字 11 到 13pt,FontWeight.Normal。卡片高度闪卡固定 140pt,确保两列对齐。间距外边距 16pt,卡片间距 12pt,元素内边距 14pt。

7.3 圆角与阴影

卡片圆角 10 到 12pt,柔和现代。头像圆角 24pt(等于宽度的一半,等于完美圆形)。按钮默认圆角,宽度 70%,高度 48pt,符合人体工程学的手指点击热区。

7.4 设计原则四条

第一,留白充足。每张卡片之间有 12pt 间距,页面四周有 16pt 内边距,避免视觉拥挤。第二,信息层级清晰。用字号、字重、颜色三重区分主次信息,让用户一眼抓住重点。第三,反馈即时。点击闪卡立即翻转,点击 Tab 立即切换状态,不让用户产生"操作失灵"的疑虑。第四,一致的视觉语言。所有卡片用相同的圆角、背景色、内边距,强化品牌识别。

7.5 鸿蒙设计规范建议

如果你想更专业,建议参考 HarmonyOS Design 规范,使用官方推荐的字体(HarmonyOS Sans)、间距栅格、色彩系统。这不仅能提升产品观感,还能在应用市场上获得官方推荐。


八、关键技术细节深度解析

8.1 卡片翻转状态管理的三个坑

在 ArkTS 中,状态管理有几个"坑"必须注意,否则代码看起来对,但 UI 就是不刷新。

坑 1:直接修改对象不触发刷新
// 错误:直接修改 @State 对象的内部状态
@State card: FlashCard = new FlashCard(1, 'Hello', '你好', '/həˈloʊ/', 'Hello, how are you?');

changeCard() {
  this.card.word = 'Hi'; // UI 不会刷新!
}

原因:ArkTS 的 @State 装饰器只监听引用变化,不监听对象内部属性变化。解决方案:创建新对象。

// 正确:创建新对象
changeCard() {
  this.card = new FlashCard(1, 'Hi', '你好', '/haɪ/', 'Hi, how are you?');
}
坑 2:Set 类型的响应式支持不完善
// 不推荐
@State flippedIds: Set<number> = new Set();

flip(id: number) {
  this.flippedIds.add(id); // UI 可能不刷新
}

解决方案:用数组加 filter 加 concat。

// 推荐:用数组 + filter/concat
@State flippedIds: number[] = [];

flip(id: number) {
  if (this.isFlipped(id)) {
    this.flippedIds = this.flippedIds.filter((v: number) => v !== id);
  } else {
    this.flippedIds = this.flippedIds.concat([id]);
  }
}
坑 3:@Builder 函数的调用方式

ArkTS 的 @Builder 装饰器有两种用法。第一,全局 @Builder,定义在组件外,用 BuilderName() 调用。第二,组件内 @Builder,定义在 struct 内,需要用 this.BuilderName() 调用。本项目用的是第二种。

@Component
struct MyComponent {
  @Builder
  MyBuilder() {
    Text('Hello')
  }

  build() {
    Column() {
      this.MyBuilder() // 通过 this 调用
    }
  }
}

8.2 评分颜色映射函数

StatusColor(score: number): string {
  if (score >= 90) {
    return '#4CAF50'; // 优秀 - 绿色
  } else if (score >= 80) {
    return '#2196F3'; // 良好 - 蓝色
  } else if (score >= 60) {
    return '#FF9800'; // 及格 - 橙色
  } else {
    return '#F44336'; // 不及格 - 红色
  }
}

设计思路:用红绿灯式的颜色映射,让用户一眼看出自己表现。阈值 90、80、60 是常见的评分体系分界线。颜色与顶部"学习概览"的三个指标色保持一致,强化视觉品牌。

这个函数同时被用在两个地方:左侧头像的背景色和右侧得分的文字色,形成视觉呼应。

8.3 列表项布局的 layoutWeight 技巧

对话记录列表项是典型的"左中右"三栏布局,关键技巧就是 layoutWeight。

Row() {
  // 左:固定宽度头像
  Column() { ... }
    .width(48)
    .height(48)

  // 中:占据剩余空间
  Column() { ... }
    .layoutWeight(1)  // 关键:占据剩余空间
    .margin({ left: 12 })

  // 右:自适应宽度
  Column() { ... }
    .alignItems(HorizontalAlign.End)
}

layoutWeight(1) 是 ArkTS 中实现"中间自适应"的核心 API,它告诉布局引擎"把所有剩余空间分配给这个组件"。当左右两栏固定宽度时,中间栏自动填满剩余空间,无论屏幕宽度如何变化。

8.4 ForEach 的 key 生成器

ForEach(this.flashcards, (item: FlashCard) => {
  // 渲染逻辑
}, (item: FlashCard) => item.id.toString())

第三个参数的作用:当数据源变化时,ArkTS 用这个 key 来判断哪些项是新增的、哪些是删除的、哪些是移动的,从而最小化 UI 刷新范围。最佳实践:key 必须是稳定且唯一的(用 id 而非 index);key 必须是字符串(toString() 一下);不要用 Math.random() 或时间戳,否则在数据变化时会导致整个列表重建。

如果数据量超过 100 条,建议用 LazyForEach 替代 ForEach。LazyForEach 会只渲染可见区域,大幅提升性能。

8.5 string.json 多语言资源化

将所有用户可见的文本放入 resources/base/element/string.json,方便后续 i18n。

{
  "string": [
    {
      "name": "module_desc",
      "value": "语言学习"
    },
    {
      "name": "EntryAbility_desc",
      "value": "21 天语言训练营"
    },
    {
      "name": "EntryAbility_label",
      "value": "语言学习"
    }
  ]
}

使用时通过 $r(‘app.string.module_desc’) 引用。当未来要出海时,只需要新增 en_US、ja_JP 等目录,把字符串翻译一遍即可。


九、性能优化与最佳实践

9.1 减少不必要的渲染

9.1.1 合理拆分组件

如果一个组件包含复杂的子树,建议拆分成独立的 @Component,这样当父组件刷新时,子组件可以通过 @Prop 或 @Link 精确控制刷新范围,避免整个树重新渲染。

9.1.2 避免内联对象
// 不推荐:每次 build 都创建新对象
ForEach(this.items, (item: Item) => {
  ListItem() {
    Text(item.name)
      .fontSize({ size: 16, weight: FontWeight.Bold } as any) // 性能差
  }
})

// 推荐:提取常量
private static readonly TITLE_STYLE: FontWeight = FontWeight.Bold;

9.2 列表性能优化

9.2.1 使用 LazyForEach 替代 ForEach

对于超长列表(大于 100 项),LazyForEach 会只渲染可见区域,大幅提升性能。本项目数据量小(6 张闪卡、5 条记录),用 ForEach 即可。

9.2.2 避免在循环中做重计算
// 不推荐
ForEach(this.items, (item: Item) => {
  Text(this.heavyCompute(item)) // 每次 build 都重算
})

// 推荐:缓存计算结果
computedMap: Map<string, string> = new Map();
heavyCompute(item: Item): string {
  if (!this.computedMap.has(item.id)) {
    this.computedMap.set(item.id, /* 重计算 */);
  }
  return this.computedMap.get(item.id)!;
}

9.3 状态管理进阶

对于更复杂的项目,建议引入 @Provide 与 @Consume 实现跨组件层级共享状态;@Observed 与 @ObjectLink 监听嵌套对象变化;@StorageLink 与 @StorageProp 实现全局持久化状态。但对于本项目(数据量小、状态简单),@State 已经足够。

什么时候该升级到全局状态管理?当多个页面需要共享同一份数据时(如用户信息、学习进度),或者数据需要跨页面持久化时(如登录态、设置项),就适合引入 AppStorage 或 LocalStorage。

9.4 资源管理

9.4.1 颜色资源化

将常用颜色放入 resources/base/element/color.json:

{
  "color": [
    { "name": "brand_primary", "value": "#1976D2" },
    { "name": "success", "value": "#4CAF50" }
  ]
}

使用时通过 $r(‘app.color.brand_primary’) 引用。

9.4.2 字符串资源化

本项目已经将 module_desc、EntryAbility_desc 等放入 string.json,方便后续多语言适配。

9.4.3 图片资源化

图标、插画等图片放入 resources/base/media/,通过 $r(‘app.media.xxx’) 引用,避免硬编码路径。

9.5 启动性能优化

第一,避免在 aboutToAppear 中做重操作。页面生命周期回调 aboutToAppear 中不要做同步的耗时操作(如读取大文件、网络请求),否则会阻塞首屏渲染。第二,懒加载非首屏内容。主页面只需要渲染首屏可见的部分,其他内容可以用 LazyForEach 懒加载。第三,预编译模板。DevEco Studio 默认开启了 AOT 预编译,无需额外配置。


十、常见问题与解决方案

Q1:点击按钮没反应,路由跳转失败?

原因:目标页面未在 main_pages.json 中注册。解决:在 main_pages.json 的 src 数组中添加目标页面路径。

Q2:@State 修改了,UI 没刷新?

原因:直接修改对象内部属性或使用了 Set 或 Map。解决:用 concat、filter 或创建新对象的方式触发引用变化。

Q3:Grid 列宽不均?

原因:columnsTemplate 写错。解决:

.columnsTemplate('1fr 1fr')  // 两列等宽
.columnsTemplate('repeat(2, 1fr)')  // 等价写法

Q4:List 列表项之间有空隙?

原因:默认 divider 有高度。解决:

List() { ... }
  .divider({ strokeWidth: 0 })  // 隐藏分割线

Q5:ForEach 报警告"the array may be updated during rendering"?

原因:在 ForEach 内部修改了被遍历的数组。解决:把修改操作放到 onClick 等事件回调中,避免在 build 阶段修改数据。

Q6:模拟器运行正常,真机崩溃?

原因:可能缺少权限或资源。解决:第一步,检查 module.json5 中的 requestPermissions。第二步,检查资源文件是否全部打包。第三步,查看真机日志 hdc shell hilog。

Q7:如何实现页面间参数传递?

通过 router.getParams() 在目标页面接收参数。

// 发送方
router.pushUrl({ url: 'pages/Detail', params: { id: 123 } });

// 接收方
@State id: number = 0;
aboutToAppear() {
  const params = router.getParams() as Record<string, Object>;
  this.id = params['id'] as number;
}

Q8:如何实现返回上一页并刷新数据?

使用 router.back() 后,在上一页的 aboutToAppear 中重新加载数据。

// 上一页
aboutToAppear() {
  this.loadData();
}

// 当前页
router.back();

十一、扩展方向与未来演进

本项目为最小可运行版本(MVP),后续可从以下几个方向扩展。

11.1 数据持久化

闪卡数据可以用 @ohos.data.preferences 存到本地,后续可同步到云端。学习记录每次 AI 对话结束后追加到 conversations 数组,再写入 Preferences。学习概览根据历史数据动态计算,不写死。

进阶方案是用 @ohos.data.relational.store 关系型数据库存储,支持 SQL 查询,更适合复杂业务。

11.2 AI 能力接入

真实 AI 对话可以用 @kit.NetworkKit 调用 LLM API(如 DeepSeek、ChatGLM、Qwen),实现真正的口语陪练。语音识别用 @kit.CoreSpeechKit 识别用户发音。语音评测用 @kit.CoreSpeechKit 的发音评估接口给用户打分,提供更客观的反馈。

11.3 学习算法

间隔重复集成 SM-2 或 Anki 算法,自动安排复习时间。艾宾浩斯曲线根据遗忘曲线动态调整闪卡出现频率。学习路径根据用户水平动态调整单词难度,循序渐进。

11.4 UI 增强

动画方面给闪卡翻转加 transition 动画,提升体验。暗黑模式适配深色主题,跟随系统设置自动切换。横屏适配用媒体查询做响应式布局,在不同设备上都有良好体验。

11.5 社交化

学习圈让用户分享学习成果。排行榜和好友比拼学习天数,激发竞争意识。小组学习组队打卡,互相监督,提升留存率。

11.6 商业化

会员体系分级权益(基础免费、Pro 会员解锁高级功能)。付费课程包提供雅思、托福、商务英语等专项训练。硬件联动与华为耳机、智能音箱联动,实现"随时随地学"。


十二、总结与展望

12.1 项目价值

「21 天语言训练营」是一个麻雀虽小、五脏俱全的 HarmonyOS ArkTS 教学项目。它用最朴素的控件(Grid、List)、最基础的状态管理(@State)、最简单的路由(router)实现了一个完整的语言学习 App。

对于 ArkTS 初学者来说,本项目的价值在于:完整性,从入口页到主功能页,从数据模型到 UI 渲染,完整演示了一个真实 App 的结构;可运行性,所有代码都经过验证,可以直接在 DevEco Studio 中跑起来;可扩展性,数据模型独立、UI 与逻辑解耦,方便后续接入真实 AI 能力。

12.2 技术收获

通过本项目,你应该掌握了以下技能。第一,ArkTS 项目结构,知道每个目录、每个配置文件的作用。第二,Grid、List 控件,理解它们的属性、用法、性能特点。第三,@State 状态管理,知道什么时候用数组、什么时候用对象、什么时候需要拆组件。第四,router 路由,掌握页面跳转的完整流程。第五,@Builder UI 抽离,学会把复杂 UI 拆成可复用片段。第六,数据模型设计,理解类、构造函数、默认值的用法。

12.3 学习建议

如果你想进一步深入 ArkTS 开发,建议按以下路径学习。第一步,官方文档,访问 HarmonyOS 开发者官网,查阅 ArkTS 与 ArkUI 的权威指南。第二步,ArkTS 语言规范,重点看类型系统与装饰器,这是 ArkTS 与 TypeScript 的关键差异。第三步,ArkUI 组件库,熟悉所有内置组件的属性与事件。第四步,状态管理 V2,学习新一代状态管理方案 @ObservedV2、@Trace。第五步,实战项目,从仿写成熟 App 开始,逐步做原创。

12.4 写在最后

HarmonyOS 生态正在快速发展,ArkTS 作为官方推荐语言,未来几年会有大量的人才需求。希望本项目能成为你鸿蒙之旅的起点,也欢迎你在此基础上做出更有创意的产品。21 天养成一个习惯,21 天入门一门新技术。愿我们都能在鸿蒙生态中找到自己的位置。


项目地址:d:\Code\Learn\ArkTS\21DayHarmonyOSTraining\ArkTSSpoken
核心页面:entry/src/main/ets/pages/LanguageLearning.ets
技术栈:ArkTS 6.1.1(24) + ArkUI + HarmonyOS

如果本文对你有帮助,欢迎点赞、收藏、评论!你的支持是我持续输出优质内容的最大动力。后续我还会写更多 ArkTS 实战教程,包括但不限于:状态管理 V2 详解、网络请求封装、原子化服务开发、跨设备流转等。敬请期待。


十三、HarmonyOS 工具链与调试技巧详解

13.1 DevEco Studio 高效开发技巧

第一,快捷键配置。DevEco Studio 基于 IntelliJ IDEA,快捷键与 WebStorm、PyCharm 一致。建议熟练掌握以下快捷键:双击 Shift 打开全局搜索、Ctrl 加 Shift 加 F 全局查找、Ctrl 加 Shift 加 R 全局替换、Alt 加 Enter 万能修复、Ctrl 加 Alt 加 L 格式化代码、Ctrl 加 / 单行注释、Ctrl 加 Shift 加 / 块注释。掌握这些快捷键能让开发效率提升至少三成。

第二,实时预览。在编辑器右上角点击 Previewer 按钮,可以打开实时预览面板,修改代码后立即看到 UI 变化,无需反复编译运行。预览面板支持切换设备型号、暗黑模式、字体大小,非常适合 UI 调优。

第三,Inspector 抓取 UI 结构。运行应用后,在 DevEco Studio 的 Hilog 中可以看到 UI 树结构。配合 ui-viewer 工具,可以抓取真实运行时的 UI 层级,方便排查布局问题。

第四,hdc 命令行调试。hdc(HarmonyOS Device Connector)是鸿蒙的设备连接工具,类似于 Android 的 adb。常用命令包括 hdc list targets 列出连接的设备、hdc shell 进入设备 shell、hdc install 安装应用、hdc file send 发送文件到设备、hdc hilog 抓取设备日志、hdc shell screencap 截屏。

13.2 常见编译错误的解决方法

错误 1:Cannot find name ‘X’。原因:变量未声明或导入缺失。解决:检查 import 语句,必要时补全类型声明。

错误 2:Type ‘X’ is not assignable to type ‘Y’。原因:类型不匹配。解决:检查函数签名与实际调用,必要时做类型转换或调整函数定义。

错误 3:Property ‘X’ does not exist on type ‘Y’。原因:访问了对象不存在的属性。解决:检查对象模型,添加缺失字段或修正属性名。

错误 4:Strict mode violation。ArkTS 默认开启严格模式,禁止使用 any、联合类型等动态类型。解决:使用具体类型或 Object 替代 any。

13.3 真机调试全流程

第一步,开启开发者模式。在手机设置 → 关于手机 → 连续点击版本号 7 次,进入开发者模式。第二步,开启 USB 调试。在开发者选项中开启 USB 调试开关。第三步,连接设备。用 USB 数据线连接手机和电脑,授权调试。第四步,选择设备。在 DevEco Studio 顶部设备选择栏中,选中已连接的真机。第五步,运行项目。点击 Run 按钮(绿色三角),应用就会安装到真机上并启动。

真机调试比模拟器更能反映真实性能,特别是涉及动画、列表滚动、网络请求等场景。建议在功能基本稳定后,及时在真机上验证体验。


十四、ArkTS 与主流跨端框架的对比

很多开发者在选型时会纠结:到底用 ArkTS、Flutter、React Native 还是 KMP(Kotlin Multiplatform)?下面从多个维度做一次系统对比。

14.1 语言与生态

ArkTS 是 TypeScript 超集,语法对前端开发者非常友好,学习成本低。Flutter 用 Dart 语言,生态集中在 Google 体系。React Native 用 JavaScript/TypeScript,生态最丰富但碎片化。KMP 用 Kotlin,主打共享业务逻辑,UI 层仍需各自实现。

14.2 性能表现

ArkTS 基于方舟编译器,原生渲染,性能最优。Flutter 自绘引擎,60fps 流畅,但在低端机上偶有掉帧。React Native 通过 Bridge 与原生通信,复杂动画时性能较差。KMP UI 层依赖原生,性能接近原生。

14.3 鸿蒙生态适配

ArkTS 是一等公民,所有鸿蒙原生能力(原子化服务、跨设备流转、AI 引擎、语音识别)直接调用,无需任何桥接。Flutter 适配鸿蒙依赖社区插件,部分能力受限。React Native 适配鸿蒙还处于早期阶段。KMP 可以通过 expect/actual 机制调用鸿蒙原生能力,但门槛较高。

14.4 学习成本

ArkTS 对前端开发者最友好,对 Android 开发者次之。Flutter 对新手友好,但需要学 Dart。React Native 对前端开发者最友好。KMP 对 Kotlin 开发者最友好。

14.5 商业化前景

从长期看,HarmonyOS 设备保有量会持续增长,ArkTS 人才缺口会持续扩大。Flutter 适合需要同时支持 Android 和 iOS 的场景。React Native 适合已有 JS 技术栈的团队。KMP 适合大型企业级项目需要最大化代码复用。

14.6 选型建议

如果你的产品只在 HarmonyOS 上跑,强烈推荐 ArkTS。如果需要同时支持 Android 和 iOS,Flutter 是更稳妥的选择。如果团队前端背景强,可以考虑 React Native。如果追求代码复用极致,可以研究 KMP。但从鸿蒙生态绑定、未来增长性、政策支持等角度看,ArkTS 是最值得投入的方向。


十五、商业化落地与产品迭代建议

15.1 MVP 阶段:从 0 到 1

MVP(Minimum Viable Product)阶段的核心是快速验证产品假设。建议的开发顺序是:第一周完成闪卡背诵模块,第二周完成 AI 对话基础框架,第三周接入真实 AI 接口,第四周打磨 UI 与体验。MVP 阶段不要追求功能完备,只关注核心闭环是否跑通。

15.2 增长阶段:从 1 到 10

增长阶段的核心是用户留存与裂变。关键指标包括:日活、月活、次日留存、7 日留存、30 日留存、付费转化率。增长抓手包括:连续打卡激励机制、分享得会员功能、学习数据可视化、AI 对话场景多样化。推荐通过 A/B 测试驱动迭代,每两周一次小版本发布。

15.3 商业化阶段:从 10 到 100

商业化阶段的核心是变现效率与用户满意度平衡。商业化路径包括:免费基础功能加付费 Pro 会员、按课程包计费、与硬件厂商联合会员、广告变现(但学习 App 慎用广告)。定价策略上,参考市面同类产品,年卡定价在 168 元到 298 元区间较为合理。

15.4 团队配置建议

MVP 阶段 2 到 3 人即可,1 名全栈 + 1 名 UI 设计 + 1 名产品。增长阶段需要扩充到 5 到 8 人,增加 AI 算法工程师、后端开发、增长运营。商业化阶段 10 人以上,需要补齐数据分析师、市场、客服等岗位。

15.5 风险与应对

技术风险:AI 接口不稳定。应对:建立降级方案,AI 不可用时使用预设对话。政策风险:内容合规、未成年人保护。应对:接入内容审核服务,建立敏感词库。竞争风险:巨头入场。应对:聚焦垂直场景(如儿童英语、商务英语),建立差异化壁垒。


十六、开发者成长路径建议

16.1 入门期(0 到 3 个月)

第一,熟读官方文档,把 ArkTS 基础语法过一遍。第二,动手复刻本项目,从零开始写一遍,加深理解。第三,阅读 3 到 5 个高质量的开源 ArkTS 项目,学习优秀实践。第四,尝试修改本项目,添加一些新功能(如收藏、搜索、统计)。

16.2 进阶期(3 到 12 个月)

第一,学习状态管理 V2(@ObservedV2、@Trace)。第二,学习网络请求(@ohos.net.http、@kit.NetworkKit)。第三,学习持久化(@ohos.data.preferences、@ohos.data.relational.store)。第四,尝试做一个完整的原创项目,从需求分析到上线运营全流程跑通。

16.3 高级期(1 年以上)

第一,学习原子化服务开发,让应用可以免安装使用。第二,学习跨设备流转,实现手机/平板/智慧屏无缝切换。第三,学习 Stage 模型与 FA 模型的差异,理解 HarmonyOS 的应用架构演进。第四,参与开源项目,贡献代码,建立个人品牌。

16.4 推荐学习资源

官方文档(developer.huawei.com)是第一手资料,质量最高。HarmonyOS 开发者社区(developer.huawei.com/consumer/cn/forum)有大量实战问答。GitHub 上搜索 arkts harmonyos 可以找到很多优秀开源项目。B 站、慕课网、极客时间上也有不少 ArkTS 视频教程。

16.5 心态建议

学习一门新技术最重要的是"开始动手",而不是"准备充分"。本项目就是一个很好的起点,不要被"我基础还不够好"的想法困住。代码是写出来的,不是想出来的。先跑起来,再优化;先实现,再完美。鸿蒙生态正在爆发期,现在入场正是最好的时机。


十七、附录:完整项目文件清单

为了方便读者快速浏览与复刻本项目,下面列出所有关键文件及其作用。

根目录文件。build-profile.json5:项目级构建配置。oh-package.json5:项目级包管理配置。hvigorfile.ts:构建脚本入口。code-linter.json5:代码检查规则。

AppScope 目录。app.json5:应用级配置(应用名、版本号、图标)。resources/base/element/string.json:应用名、描述等字符串资源。

entry 模块。src/main/module.json5:模块清单,定义入口 Ability 与设备类型。src/main/ets/entryability/EntryAbility.ets:Ability 生命周期入口。src/main/ets/pages/Index.ets:欢迎页与路由跳转入口。src/main/ets/pages/LanguageLearning.ets:主功能页(Grid 闪卡 + List 对话记录)。src/main/resources/base/profile/main_pages.json:页面路由注册。src/main/resources/base/element/string.json:模块字符串资源。src/main/resources/base/element/color.json:颜色资源。src/main/resources/base/element/float.json:尺寸资源。

测试相关。src/ohosTest/ets/test/Ability.test.ets:Ability 端到端测试。src/ohosTest/ets/test/List.test.ets:列表组件测试。src/test/LocalUnit.test.ets:本地单元测试。src/test/List.test.ets:列表单元测试。src/mock/mock-config.json5:测试 mock 数据配置。

每个文件都在 HarmonyOS 应用中扮演特定角色,缺一不可。理解这些文件的用途,能帮你快速定位问题,也为后续扩展打下基础。


十八、写在最后的话

HarmonyOS 代表着下一代操作系统的方向。ArkTS 是这个生态的官方语言,掌握它不仅是技术能力的提升,更是搭上了鸿蒙生态的快车。语言学习是一个长青赛道,把 ArkTS 能力与语言学习产品结合,既能打磨技术,又能创造真实价值,这正是本项目最大的意义。

希望本文不仅是一篇教程,更是一份启发。如果你读完之后产生了"我也想做一个自己的 ArkTS 项目"的冲动,那么本项目就完成了它的使命。

21 天入门 ArkTS,21 天养成学习习惯。从今天开始,从一行代码开始,从你的第一个 Grid 控件开始。

愿每一位读者都能在 HarmonyOS 生态中找到自己的位置,做出有价值的产品,收获属于自己的成就感。鸿蒙生态,未来可期。让我们一起砥砺前行,共同成长。

Logo

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

更多推荐