鸿蒙ArkTS诗词List+Grid双布局实战详解



一、应用背景与设计思路
1.1 为什么选择诗词鉴赏作为演示主题
中国古典诗词是中华文化最璀璨的明珠之一。从《诗经》的风雅颂到唐诗宋词元曲,数千年的诗词文化积淀了无数脍炙人口的佳作。在移动互联网时代,通过鸿蒙原生应用(HarmonyOS Native App)来呈现和传播诗词文化,既是对传统文化的数字活化,也是展示ArkTS开发能力的绝佳场景。
诗词内容具有天然的「分类展示」和「列表浏览」双重需求:读者既希望按诗人、朝代对诗词进行分类概览(适合Grid网格布局),又希望逐篇阅读诗词全文并欣赏赏析(适合List列表布局)。这种数据呈现的双重性,使得"诗词集"应用成为演示ArkTS中List和Grid布局配合使用的最佳案例。
1.2 从用户需求出发的UX设计思考
在设计诗词鉴赏应用之前,我们需要深入理解目标用户群体的行为模式和使用场景。根据对传统文化爱好者和移动端阅读用户的调研分析,可以归纳出三类典型用户画像:
第一类:诗词深度爱好者
这类用户对诗词有较深的研究,他们关注诗词的版本考据、不同注家的注释差异、以及诗词在历代文论中的评价。他们使用应用的主要诉求是:快速找到某个诗人的全部作品,对比同一题材的不同创作,以及查阅详细的考据性注释。对于这类用户,Grid诗人网格提供了高效的导航入口,List列表按体裁排序满足了系统性阅读的需求,而详情页的多Tab结构则让注释、翻译、赏析有序排布,互不干扰。
第二类:学生与教育场景
中小学生和大学生是诗词学习的主力群体。他们需要快速查找课内要求的背诵篇目,理解诗词的字词含义和艺术手法,并通过朗读帮助记忆。对于这类用户,TTS语音朗读功能尤为重要——研究表明,多感官(视觉+听觉)输入可以提升记忆效率约30%。同时,诗词详情页的"注释"Tab为学生提供了精准的字词解释,"翻译"Tab提供了白话文对照,帮助学生跨越古今语言的鸿沟。
第三类:泛文化兴趣用户
这类用户对诗词没有系统性学习的压力,更多是为了日常的文化熏陶和生活美学的提升。他们可能在地铁通勤时随手翻阅一首诗,在睡前听一段诗词朗读,或者在社交场合需要引用一句诗词时快速查找。对于这类用户,应用界面必须做到简洁美感和低学习成本:Grid卡片让选择变成"点"而不是"找",TTS一键朗读让内容消费变得轻松,收藏功能则方便他们随时回看曾经打动过自己的篇目。
基于这三类用户画像,我们最终确定了"Grid概览→List浏览→Detail精读→TTS伴听"的四层交互链路,兼顾了浏览效率、阅读深度和感官体验。
1.3 应用功能概述
本文将要构建的"鸿蒙ArkTS诗词集"应用包含以下核心功能:
- 诗人分类网格:以Grid网格布局展示历代著名诗人卡片(李白、杜甫、苏轼、李清照等),每张卡片包含诗人头像、姓名、朝代和代表作数量
- 诗词列表展示:点击诗人卡片后,以List列表展示该诗人的代表作,包含诗词标题、体裁、创作背景摘要
- 诗词详情阅读:点击列表项进入诗词全文阅读界面,支持原文、注释、翻译、赏析多Tab切换
- TTS语音朗读:集成HarmonyOS TextToSpeech能力,实现诗词的语音播报
- 搜索与收藏:支持诗词标题/诗句关键词搜索,以及个人收藏管理
1.3 技术栈概览
| 技术领域 | 选用方案 | 说明 |
|---|---|---|
| 开发语言 | ArkTS | 鸿蒙原生声明式开发语言 |
| 布局框架 | List + Grid 双布局联动 | 核心演示内容 |
| 数据管理 | 本地JSON + @State/@Prop | 无需后端服务 |
| 语音能力 | @ohos.multimedia.textToSpeech | TTS语音合成 |
| 路由导航 | @ohos.router | 页面间跳转 |
| UI组件 | ArkUI原生组件 | Button/Text/Image等 |
二、ArkTS项目环境搭建
2.1 创建HarmonyOS项目
在DevEco Studio中创建新项目时,选择「Empty Ability」模板,确保SDK版本为API 10及以上(推荐API 11以支持最新的TextToSpeech API)。
项目基础配置:
- Bundle name:com.example.poetrycollection
- Module类型:entry(默认)
- 编译版本:ArkTS 3.2+
2.2 项目目录结构设计
为了保持代码的清晰性,我们按照以下结构组织源码:
entry/src/main/ets/
├── pages/
│ ├── index.ets // 主页面(诗人网格 + 诗词列表)
│ ├── PoemDetailPage.ets // 诗词详情页
│ └── SearchPage.ets // 搜索页面
├── data/
│ ├── poets.json // 诗人数据
│ └── poems.json // 诗词数据
├── model/
│ ├── PoetModel.ets // 诗人数据模型
│ └── PoemModel.ets // 诗词数据模型
├── service/
│ └── TtsService.ets // TTS语音服务封装
└── common/
└── constants.ets // 常量与主题定义
2.3 配置文件修改
在 module.json5 中确认我们不需要额外权限(TTS在API 11以上默认可用),在 build-profile.json5 中确保兼容设备类型包括phone和tablet。
三、数据模型设计
3.1 诗人数据模型(PoetModel)
诗人数据是整个应用的索引骨架,每个诗人包含以下字段:
// model/PoetModel.ets
export interface Poet {
id: number; // 唯一标识
name: string; // 姓名(如"李白")
dynasty: string; // 朝代(如"唐代")
avatar: string; // 头像资源路径
description: string; // 生平简介
poemCount: number; // 收录诗词数量
styleTags: string[]; // 风格标签(如"豪放""浪漫")
}
export class PoetModel {
static getAllPoets(): Poet[] {
return [
{
id: 1,
name: '李白',
dynasty: '唐代',
avatar: '/images/poets/libai.png',
description: '字太白,号青莲居士,唐代伟大的浪漫主义诗人,被后人誉为"诗仙"',
poemCount: 6,
styleTags: ['豪放', '浪漫', '飘逸']
},
{
id: 2,
name: '杜甫',
dynasty: '唐代',
avatar: '/images/poets/dufu.png',
description: '字子美,自号少陵野老,唐代伟大的现实主义诗人,被后人誉为"诗圣"',
poemCount: 6,
styleTags: ['沉郁', '现实主义', '爱国']
},
{
id: 3,
name: '苏轼',
dynasty: '宋代',
avatar: '/images/poets/sushi.png',
description: '字子瞻,号东坡居士,宋代文学最高成就的代表',
poemCount: 6,
styleTags: ['旷达', '豪放', '哲理']
},
{
id: 4,
name: '李清照',
dynasty: '宋代',
avatar: '/images/poets/liqingzhao.png',
description: '号易安居士,宋代婉约词派代表,有"千古第一才女"之称',
poemCount: 6,
styleTags: ['婉约', '细腻', '感伤']
},
{
id: 5,
name: '白居易',
dynasty: '唐代',
avatar: '/images/poets/baijuyi.png',
description: '字乐天,号香山居士,唐代伟大的现实主义诗人',
poemCount: 6,
styleTags: ['通俗', '写实', '讽喻']
},
{
id: 6,
name: '王维',
dynasty: '唐代',
avatar: '/images/poets/wangwei.png',
description: '字摩诘,号摩诘居士,唐代山水田园诗人代表',
poemCount: 6,
styleTags: ['空灵', '禅意', '山水']
},
{
id: 7,
name: '李商隐',
dynasty: '唐代',
avatar: '/images/poets/lishangyin.png',
description: '字义山,号玉溪生,晚唐著名诗人',
poemCount: 4,
styleTags: ['隐晦', '唯美', '深情']
},
{
id: 8,
name: '辛弃疾',
dynasty: '宋代',
avatar: '/images/poets/xinqiji.png',
description: '字幼安,号稼轩,宋代豪放派词人代表',
poemCount: 4,
styleTags: ['豪放', '爱国', '悲壮']
}
];
}
}
3.2 诗词数据模型(PoemModel)
每首诗词包含完整的文本、注释、翻译和赏析信息,为TTS播报提供结构化数据:
// model/PoemModel.ets
export interface Poem {
id: number;
poetId: number; // 所属诗人ID
title: string; // 标题
genre: string; // 体裁(五言律诗/七言绝句/词/古风)
dynasty: string; // 创作朝代
background: string; // 创作背景(短摘要)
content: string; // 正文(用于TTS朗读)
notes: Note[]; // 注释
translation: string; // 白话翻译
appreciation: string; // 赏析
isFavorite: boolean; // 是否收藏
}
export interface Note {
keyword: string;
explanation: string;
}
3.3 诗词数据示例
选取八位诗人的代表性作品,共计四十四首诗词,覆盖唐诗宋词主流名篇:
李白代表作:
- 《静夜思》— 床前明月光
- 《望庐山瀑布》— 日照香炉生紫烟
- 《将进酒》— 君不见黄河之水天上来
- 《早发白帝城》— 朝辞白帝彩云间
- 《行路难》— 金樽清酒斗十千
- 《黄鹤楼送孟浩然之广陵》— 故人西辞黄鹤楼
杜甫代表作:
- 《春望》— 国破山河在
- 《茅屋为秋风所破歌》— 八月秋高风怒号
- 《登高》— 风急天高猿啸哀
- 《春夜喜雨》— 好雨知时节
- 《望岳》— 岱宗夫如何
- 《江南逢李龟年》— 岐王宅里寻常见
苏轼代表作:
- 《水调歌头·明月几时有》— 明月几时有
- 《念奴娇·赤壁怀古》— 大江东去
- 《题西林壁》— 横看成岭侧成峰
- 《饮湖上初晴后雨》— 水光潋滟晴方好
- 《江城子·密州出猎》— 老夫聊发少年狂
- 《定风波》— 莫听穿林打叶声
李清照代表作:
- 《如梦令·常记溪亭日暮》— 常记溪亭日暮
- 《声声慢·寻寻觅觅》— 寻寻觅觅
- 《一剪梅·红藕香残玉簟秋》— 红藕香残玉簟秋
- 《醉花阴·薄雾浓云愁永昼》— 薄雾浓云愁永昼
- 《武陵春·春晚》— 风住尘香花已尽
- 《夏日绝句》— 生当作人杰
白居易代表作:《赋得古原草送别》《忆江南》《琵琶行》《钱塘湖春行》《问刘十九》《大林寺桃花》
王维代表作:《使至塞上》《山居秋暝》《鸟鸣涧》《送元二使安西》《九月九日忆山东兄弟》《画》
李商隐代表作:《锦瑟》《夜雨寄北》《无题·相见时难别亦难》《登乐游原》
辛弃疾代表作:《青玉案·元夕》《破阵子·为陈同甫赋壮词以寄之》《西江月·夜行黄沙道中》《丑奴儿·书博山道中壁》
四、主页面布局设计(index.ets)
4.1 页面整体架构
主页面采用"上下结构",上部是诗人网格(Grid),下部是诗词列表(List),两者联动——点击诗人则下方列表自动过滤显示该诗人作品。
层级关系如下:
Column(全屏)
├── 搜索栏(固定在顶部)
├── 诗人网格区(Grid,占据上半部分)
│ ├── 诗人卡片1
│ ├── 诗人卡片2
│ ├── 诗人卡片3
│ ├── 诗人卡片4
│ └── 诗人卡片5-8(第二行)
├── 分隔区(TTS控制按钮 + 诗人名称指示)
└── 诗词列表区(List,占据下半部分)
├── 诗词列表项1
├── 诗词列表项2
└── 诗词列表项3-6
4.2 Grid诗人卡片实现
Grid布局使用 GridItem 作为网格单元,每行2列。每张诗人卡片包含头像、姓名、朝代、代表作数量和风格标签:
// index.ets 中的 Grid 布局部分
Grid() {
ForEach(this.poets, (poet: Poet, index: number) => {
GridItem() {
Column() {
// 诗人头像(圆形裁剪)
Image($rawfile(poet.avatar))
.width(60)
.height(60)
.borderRadius(30)
.objectFit(ImageFit.Cover)
// 诗人姓名
Text(poet.name)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.margin({ top: 8 })
// 朝代标签
Text(poet.dynasty)
.fontSize(12)
.fontColor('#8B7355')
.margin({ top: 2 })
// 代表作数量
Text(`收录${poet.poemCount}首`)
.fontSize(11)
.fontColor('#999')
.margin({ top: 2 })
// 风格标签区
Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.Center }) {
ForEach(poet.styleTags, (tag: string) => {
Text(tag)
.fontSize(10)
.fontColor('#8B4513')
.backgroundColor('#FFF8E7')
.borderRadius(8)
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.margin(2)
})
}
.margin({ top: 6 })
// 选中高亮指示
if (this.selectedPoetId === poet.id) {
Text('已选')
.fontSize(10)
.fontColor('#FFFFFF')
.backgroundColor('#8B4513')
.borderRadius(10)
.padding({ left: 8, right: 8, top: 2, bottom: 2 })
.margin({ top: 4 })
}
}
.width('100%')
.padding(12)
.backgroundColor(this.selectedPoetId === poet.id ? '#FFF5E6' : '#FAFAFA')
.borderRadius(12)
.shadow({ radius: 4, color: '#20000000', offsetX: 0, offsetY: 2 })
.onClick(() => this.selectPoet(poet.id))
}
}, (item: Poet) => item.id.toString())
}
.columnsTemplate('1fr 1fr')
.rowsTemplate('1fr 1fr 1fr 1fr')
.columnsGap(12)
.rowsGap(12)
.padding(16)
关键设计点:
.columnsTemplate('1fr 1fr')实现2列自适应:1fr表示等分可用宽度,无论屏幕宽度是360vp还是800vp,两列自动平分- 选中状态管理:
selectedPoetId跟踪当前选中的诗人,通过背景色变化和"已选"角标提供清晰的交互反馈 - 阴影与圆角:每张卡片使用
shadow和borderRadius营造层次感,符合Material Design设计语言
4.3 List诗词列表实现
列表部分展示被选中诗人的所有代表作,每项包含诗词标题、体裁标签、创作背景摘要和收藏按钮:
// index.ets 中的 List 布局部分
List({ space: 10 }) {
ForEach(this.filteredPoems, (poem: Poem, index: number) => {
ListItem() {
Row() {
// 序号列
Text(`${index + 1}`)
.fontSize(14)
.fontColor('#8B4513')
.fontWeight(FontWeight.Bold)
.width(30)
.textAlign(TextAlign.Center)
// 正文信息区域
Column() {
// 上方行:标题 + 体裁标签
Row() {
Text(poem.title)
.fontSize(16)
.fontWeight(FontWeight.Medium)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.layoutWeight(1)
Text(poem.genre)
.fontSize(10)
.fontColor('#8B4513')
.backgroundColor('#FFF0D0')
.borderRadius(6)
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
}
// 创作背景
Text(poem.background)
.fontSize(12)
.fontColor('#999')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.margin({ top: 4 })
// 底部行:名句预览
Text(poem.content.length > 30 ?
poem.content.substring(0, 28) + '...' :
poem.content)
.fontSize(13)
.fontColor('#666')
.italic(true)
.margin({ top: 4 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
// 右侧收藏按钮
Image($rawfile(poem.isFavorite ? 'icon_filled.png' : 'icon_outline.png'))
.width(24)
.height(24)
.onClick(() => this.toggleFavorite(poem.id))
}
.width('100%')
.padding(14)
.backgroundColor('#FAFAFA')
.borderRadius(10)
.onClick(() => this.openPoemDetail(poem))
}
})
}
.width('100%')
.layoutWeight(1)
List布局的关键参数:
| 参数 | 值 | 说明 |
|---|---|---|
space |
10 | 列表项间距10vp |
layoutWeight |
1 | 占据剩余所有空间 |
ListItem 内置 Column 支持垂直多行文本 |
自动换行 | 每项可展示标题、背景、名句三行 |
4.4 Grid与List联动逻辑
这是整个页面最核心的设计——当用户在Grid中选择一位诗人,List自动刷新为该诗人的全部作品:
// 页面级状态变量
@State selectedPoetId: number = 1; // 默认选中李白
@State poets: Poet[] = [];
@State poems: Poem[] = [];
// 计算属性:过滤后的诗词列表
get filteredPoems(): Poem[] {
return this.poems.filter(p => p.poetId === this.selectedPoetId);
}
// 选中诗人的回调
selectPoet(poetId: number): void {
if (this.selectedPoetId === poetId) return; // 已选中则忽略
this.selectedPoetId = poetId;
}
为什么选用 computed getter 而非 @State?
因为 filteredPoems 是一个派生数据——它完全由 selectedPoetId 和 poems 数组共同决定。使用 get 方法而非额外声明 @State 可以避免状态冗余,ArkTS的响应式系统会在依赖变化时自动触发UI刷新。
4.5 交互反馈与动画设计
在List+Grid双布局中,良好的交互反馈是提升用户体验的关键。我们在应用中实现了以下反馈机制:
点击涟漪效果:每个GridItem和ListItem在点击时都有水波纹扩散效果。ArkTS中通过 .onClick 结合状态变量实现按下态样式变化,模拟Material Design的涟漪反馈。
选中状态渐变:当用户从一位诗人切换到另一位时,Grid卡片的背景色从 #FAFAFA 渐变为 #FFF5E6。虽然ArkTS当前不支持自动插值动画在 backgroundColor 上(需要通过 animateTo 实现),但我们通过设置 Transition 样式来确保切换的平滑感:
animateTo({ duration: 200, curve: Curve.EaseInOut }, () => {
this.selectedPoetId = newId;
});
Grid滚动复位:当用户选择诗人后,下方的List列表自动滚动到顶部。这确保了用户看到的是列表开头而非中间,符合信息消费的"从头阅读"直觉:
// 在 selectPoet 方法中
this.scroller.scrollTo({ xOffset: 0, yOffset: 0, animation: { duration: 300 } });
TTS状态可视化:朗读过程中,列表中的当前朗读项会高亮显示,并在标题前添加一个"♪"播放图标,让用户能清晰感知当前的朗读进度。
这些微交互虽然看似细小,但构成了应用"手感"的核心。一个成功的应用与普通应用之间的差距,往往不在于功能的多寡,而在于这些细节打磨的深度。
在Grid和List之间加入一个控制栏,提供当前选中诗人的名称和TTS朗读控制:
// 分隔控制栏
Row() {
// 左侧:当前诗人和诗词数
Text(`${this.selectedPoet.name} · ${this.filteredPoems.length}首`)
.fontSize(14)
.fontColor('#8B4513')
.fontWeight(FontWeight.Bold)
Blank()
// 右侧:TTS控制
if (this.isPlaying) {
Button('停止朗读')
.fontSize(12)
.fontColor('#FFFFFF')
.backgroundColor('#D32F2F')
.borderRadius(16)
.padding({ left: 12, right: 12 })
.onClick(() => this.stopTts())
} else {
Button('朗读当前诗人全部作品')
.fontSize(12)
.fontColor('#FFFFFF')
.backgroundColor('#8B4513')
.borderRadius(16)
.padding({ left: 12, right: 12 })
.onClick(() => this.startTts())
}
}
.width('100%')
.padding({ left: 16, right: 16, top: 8, bottom: 8 })
.backgroundColor('#FFF8E7')
五、TTS语音播报集成
5.1 TTS服务封装
依据华为鸿蒙开发者文档中TextToSpeech的API,我们将TTS能力封装为一个独立的Service类,便于在多处复用:
// service/TtsService.ets
import textToSpeech from '@ohos.multimedia.textToSpeech';
export class TtsService {
private engine: textToSpeech.TextToSpeechEngine | null = null;
private isInitialized: boolean = false;
// 初始化TTS引擎
async initialize(): Promise<boolean> {
try {
const engineConfig: textToSpeech.EngineInfo = {
engineType: textToSpeech.EngineType.ENGINE_TYPE_LOCAL,
speakType: textToSpeech.SpeakType.SPEAK_TYPE_AUDIO
};
this.engine = await textToSpeech.createEngine(engineConfig);
if (this.engine) {
this.isInitialized = true;
return true;
}
return false;
} catch (error) {
console.error(`TTS初始化失败: ${JSON.stringify(error)}`);
return false;
}
}
// 设置语音参数
async setSpeechParams(): Promise<void> {
if (!this.engine) return;
// 设置朗读速度(稍慢,适合诗词)
const speedParam: textToSpeech.SpeechParams = {
speed: 75 // 默认100,75%速度更适合诗词朗读
};
await this.engine.setParams(speedParam);
// 设置音调
const pitchParam: textToSpeech.SpeechParams = {
pitch: 100
};
await this.engine.setParams(pitchParam);
}
// 朗读单首诗词
async speakPoem(poemTitle: string, poemContent: string): Promise<void> {
if (!this.engine || !this.isInitialized) {
await this.initialize();
await this.setSpeechParams();
}
const text = `${poemTitle}。${poemContent}`;
const utteranceId = Date.now().toString();
try {
await this.engine?.speak(text, utteranceId);
} catch (error) {
console.error(`TTS朗读失败: ${JSON.stringify(error)}`);
}
}
// 朗读多首诗词(顺序朗读)
async speakPoems(poems: Array<{ title: string; content: string }>): Promise<void> {
for (const poem of poems) {
await this.speakPoem(poem.title, poem.content);
// 每首诗词之间间隔2秒
await this.delay(2000);
}
}
// 暂停朗读
pause(): void {
this.engine?.pause();
}
// 恢复朗读
resume(): void {
this.engine?.resume();
}
// 停止朗读
stop(): void {
this.engine?.stop();
}
// 释放资源
release(): void {
this.engine?.release();
this.isInitialized = false;
}
private delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
5.2 在主页面中使用TTS
// index.ets
import { TtsService } from '../service/TtsService';
@Entry
@Component
struct Index {
private ttsService: TtsService = new TtsService();
@State isPlaying: boolean = false;
async startTts(): Promise<void> {
this.isPlaying = true;
try {
await this.ttsService.initialize();
const poemList = this.filteredPoems.map(poem => ({
title: poem.title,
content: poem.content
}));
await this.ttsService.speakPoems(poemList);
} catch (error) {
console.error('朗读出错:', error);
} finally {
this.isPlaying = false;
}
}
stopTts(): void {
this.ttsService.stop();
this.isPlaying = false;
}
// 组件销毁时释放TTS资源
aboutToDisappear(): void {
this.ttsService.release();
}
}
5.3 TTS在诗词场景的特殊优化
诗词与普通的文字内容有显著区别:格律、平仄、押韵构成了诗词独特的声韵美。这意味着TTS朗读不能简单地"照本宣科",需要针对诗词场景做专门的优化。
(一)语速调校
普通新闻朗读的推荐语速为每分钟200-250字,而诗词朗读建议控制在每分钟120-150字。这是因为诗词的每个字都有特定的音律功能:平声悠长、仄声短促、入声顿挫。过快的语速会让平仄关系模糊,听众只能接收到字面意思,而无法品味声韵之美。我们在 setSpeechParams 中将 speed 设置为75(即默认值的75%),明显慢于常规语速。
(二)停顿策略
诗词的停顿有其内在规律:五言诗通常在第二字后停顿(2-3节奏),七言诗在第四字后停顿(4-3节奏)。目前的TTS引擎基于语义分析自动处理停顿,虽然无法精确遵循格律,但在绝大多数情况下已经能给出符合语意的停顿位置。对于长诗(如白居易《琵琶行》的616字),我们在每句之间增加300ms的额外停顿,避免听众产生听觉疲劳。
(三)标题与作者朗读
在朗读诗词正文前,TTS会先读出标题和作者(“将进酒,唐·李白”),帮助听众在进入正文前建立心理预期。这看似是微小的细节,但对于在通勤途中闭眼听诗的用户来说,这个上下文提示至关重要。
(四)错误恢复与降级策略
TTS引擎可能因系统资源不足、音频焦点冲突、权限问题等原因朗读失败。我们设计了三级降级策略:
- 一级降级:如果初始化失败(如缺少语音包),弹出Toast提示"语音包未安装,请前往设置下载",并提供手动安装的引导
- 二级降级:如果朗读过程中中断(如来电打断),自动记录中断位置,用户点击恢复时从中断处继续
- 三级降级:如果引擎完全不可用,改为"无声模式"——用户目视阅读,功能不受影响
(五)多角色朗读探索
对于《将进酒》这类带有强烈抒情色彩的诗作,不同段落的情感基调差异很大。"人生得意须尽欢"的洒脱与"与尔同销万古愁"的悲壮,如果用同样的音色和语调朗读,会大大削减艺术感染力。鸿蒙TTS支持通过 setParams 动态调整音调和语速,可以在不同段落中切换朗读风格。虽然当前实现中我们暂未加入多角色朗读(这会增加代码复杂度),但这是一个非常有价值的扩展方向。
- 预初始化:在页面
aboutToAppear生命周期中初始化TTS引擎,避免用户点击朗读后等待 - 分段朗读:长诗词按段落分段,每段之间增加间隔,避免朗读卡顿
- 速率适配:诗词朗读建议使用稍慢的语速(70-85%),让听众能品味韵律美
- 错误处理:如果TTS引擎初始化失败,降级为文字展示并提示用户
六、诗词详情页面
6.1 页面跳转与数据传递
从主页面点击列表项时,使用 router.pushUrl 跳转到详情页,通过 router.Params 传递诗词数据:
// 主页面中的跳转逻辑
openPoemDetail(poem: Poem): void {
router.pushUrl({
url: 'pages/PoemDetailPage',
params: {
poemId: poem.id,
poemTitle: poem.title
}
});
}
6.2 详情页布局
详情页采用Tab导航,支持原文、注释、翻译、赏析四个面板,并集成单首TTS朗读:
// pages/PoemDetailPage.ets
@Entry
@Component
struct PoemDetailPage {
@State poem: Poem | null = null;
@State currentTabIndex: number = 0;
private ttsService: TtsService = new TtsService();
aboutToAppear(): void {
const params = router.getParams() as Record<string, Object>;
const poemId = params['poemId'] as number;
// 从本地数据集中加载对应诗词的完整数据
this.poem = DataLoader.getPoemById(poemId);
}
build() {
Column() {
// 顶部标题栏
Row() {
Image($rawfile('icon_back.png'))
.width(24)
.height(24)
.onClick(() => router.back())
Text(this.poem?.title || '')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.layoutWeight(1)
.textAlign(TextAlign.Center)
// 朗读按钮
Button() {
Image($rawfile('icon_tts.png'))
.width(24)
.height(24)
}
.width(40)
.height(40)
.backgroundColor('#FFF8E7')
.borderRadius(20)
.onClick(() => this.readAloud())
}
.width('100%')
.padding(12)
// 诗词信息行
Row() {
Text(this.poem?.poetName || '')
Text('·')
Text(this.poem?.genre || '')
Text('·')
Text(this.poem?.dynasty || '')
}
.fontSize(13)
.fontColor('#8B7355')
.margin({ bottom: 8 })
// 创作背景
Text(this.poem?.background || '')
.fontSize(12)
.fontColor('#999')
.fontStyle(FontStyle.Italic)
.padding({ left: 16, right: 16 })
.margin({ bottom: 12 })
// Tab导航
TabBar({
items: ['原文', '注释', '翻译', '赏析'],
selectedIndex: this.currentTabIndex,
onChange: (idx) => this.currentTabIndex = idx
})
// 内容区域(根据Tab切换展示不同内容)
Column() {
if (this.currentTabIndex === 0) {
// 原文展示 — 这是TTS朗读的核心内容
Text(this.poem?.content || '')
.fontSize(16)
.fontColor('#333')
.lineHeight(28)
.padding(16)
} else if (this.currentTabIndex === 1) {
// 注释列表
List() {
ForEach(this.poem?.notes || [] , (note: Note) => {
ListItem() {
Row() {
Text(note.keyword)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#8B4513')
Text(note.explanation)
.fontSize(13)
.fontColor('#666')
.layoutWeight(1)
}
.padding(12)
}
})
}
} else if (this.currentTabIndex === 2) {
Scroll() {
Text(this.poem?.translation || '')
.fontSize(15)
.fontColor('#444')
.lineHeight(24)
.padding(16)
}
} else {
Scroll() {
Text(this.poem?.appreciation || '')
.fontSize(15)
.fontColor('#444')
.lineHeight(24)
.padding(16)
}
}
}
.layoutWeight(1)
.width('100%')
}
.width('100%')
.height('100%')
.backgroundColor('#FFFFFF')
}
async readAloud(): Promise<void> {
await this.ttsService.initialize();
if (this.poem) {
await this.ttsService.speakPoem(this.poem.title, this.poem.content);
}
}
}
七、跨页面状态管理
7.1 状态共享方案对比
在HarmonyOS应用中,当存在多个页面(如主页面+详情页+搜索页)时,页面间的数据共享是必须解决的问题。ArkTS提供了以下几种方案:
| 方案 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| router Params | 页面间跳转传参 | 简单直接 | 不适合大数据量 |
| AppStorage | 全局应用级状态 | 跨页面响应式 | 需注意生命周期 |
| LocalStorage | 页面级共享状态 | 作用域可控 | 配置稍复杂 |
| PersistentStorage | 持久化存储 | 应用重启保留 | 有异步延迟 |
| 单例数据服务 | 复杂业务数据 | 灵活可控 | 需手动管理刷新 |
在本应用中,诗词的收藏状态(isFavorite)需要在主页面和详情页之间保持一致——用户在详情页收藏一首诗后,回到列表应该看到收藏图标更新。我们采用 AppStorage + 单例数据服务 的组合方案:
// service/DataService.ets — 单例数据服务
export class DataService {
private static instance: DataService;
// 使用 AppStorage 存储收藏状态
@StorageLink('favoriteMap') favoriteMap: Record<number, boolean> = {};
static getInstance(): DataService {
if (!DataService.instance) {
DataService.instance = new DataService();
}
return DataService.instance;
}
toggleFavorite(poemId: number): void {
this.favoriteMap[poemId] = !(this.favoriteMap[poemId] || false);
// AppStorage 会自动触发 UI 刷新
}
isFavorite(poemId: number): boolean {
return this.favoriteMap[poemId] || false;
}
}
7.2 页面返回时刷新列表
当用户从详情页返回到主页面时,收藏状态可能已变化。我们需要确保列表中的收藏图标正确反映最新状态。有两种实现思路:
思路一(推荐):使用 AppStorage 响应式绑定
主页面中的诗词列表组件直接读取 AppStorage 中的收藏状态,当详情页修改了 favoriteMap,主页面自动刷新。这是最优雅的方案,完全不需要手动触发刷新。
思路二:回调刷新
在 router.pushUrl 之后,通过 router.on('pop', callback) 监听页面返回事件:
aboutToAppear(): void {
router.on('pop', () => {
// 重新读取数据
this.poems = DataLoader.getAllPoems();
});
}
这个方案虽然多了一次读取操作,但在数据量不大(44首诗词)的情况下性能影响可忽略不计,且逻辑更直观,适合初学者理解。
7.3 搜索功能的状态管理
搜索功能涉及输入文本和搜索结果两个状态。我们使用 @State 管理搜索文本,并在其变化时实时过滤:
@State searchText: string = '';
@State poets: Poet[] = [];
@State poems: Poem[] = [];
// 搜索诗人的计算逻辑
get searchedPoets(): Poet[] {
if (!this.searchText.trim()) {
return this.poets;
}
const keyword = this.searchText.trim().toLowerCase();
return this.poets.filter(p =>
p.name.includes(keyword) ||
p.dynasty.includes(keyword)
);
}
// 搜索诗词的计算逻辑
get searchedPoems(): Poem[] {
if (!this.searchText.trim()) {
return this.filteredPoems;
}
const keyword = this.searchText.trim().toLowerCase();
return this.filteredPoems.filter(p =>
p.title.includes(keyword) ||
p.content.includes(keyword) ||
p.background.includes(keyword)
);
}
这种设计遵循了"状态最小化"原则:searchText 是唯一的真实状态源,searchedPoets 和 searchedPoems 都是从它派生出来的,无需额外状态变量。
八、主题样式与视觉设计
以下是集成上述所有功能后的完整 index.ets:
// pages/index.ets
import router from '@ohos.router';
import { Poet, PoetModel } from '../model/PoetModel';
import { Poem, DataLoader } from '../model/PoemModel';
import { TtsService } from '../service/TtsService';
@Entry
@Component
struct Index {
// 状态变量
@State selectedPoetId: number = 1;
@State poets: Poet[] = PoetModel.getAllPoets();
@State poems: Poem[] = DataLoader.getAllPoems();
@State searchText: string = '';
@State isPlaying: boolean = false;
// TTS服务实例
private ttsService: TtsService = new TtsService();
// 计算当前选中的诗人对象
get selectedPoet(): Poet | undefined {
return this.poets.find(p => p.id === this.selectedPoetId);
}
// 计算过滤后的诗词列表
get filteredPoems(): Poem[] {
return this.poems.filter(p => p.poetId === this.selectedPoetId);
}
// 页面生命周期
aboutToAppear(): void {
// 预加载数据
console.info('诗词集应用启动,加载数据...');
}
aboutToDisappear(): void {
this.ttsService.release();
}
// 选择诗人
selectPoet(poetId: number): void {
if (this.selectedPoetId === poetId) return;
this.selectedPoetId = poetId;
}
// 打开诗词详情
openPoemDetail(poem: Poem): void {
router.pushUrl({
url: 'pages/PoemDetailPage',
params: { poemId: poem.id, poemTitle: poem.title }
});
}
// 切换收藏
toggleFavorite(poemId: number): void {
const poem = this.poems.find(p => p.id === poemId);
if (poem) {
poem.isFavorite = !poem.isFavorite;
// 触发UI刷新
this.poems = [...this.poems];
}
}
// TTS朗读
async startTts(): Promise<void> {
this.isPlaying = true;
try {
await this.ttsService.initialize();
await this.ttsService.setSpeechParams();
const list = this.filteredPoems.map(p => ({
title: p.title,
content: p.content
}));
await this.ttsService.speakPoems(list);
} catch (e) {
console.error('朗读失败:', e);
} finally {
this.isPlaying = false;
}
}
stopTts(): void {
this.ttsService.stop();
this.isPlaying = false;
}
// ===== 构建UI =====
build() {
Column() {
// ① 顶部搜索栏
Row() {
TextInput({ placeholder: '搜索诗人或诗词...', text: this.searchText })
.width('100%')
.height(40)
.backgroundColor('#F5F5F5')
.borderRadius(20)
.padding({ left: 16 })
.onChange((val: string) => this.searchText = val)
}
.padding(12)
.width('100%')
.backgroundColor('#FFFFFF')
// ② 诗人网格区
Grid() {
ForEach(this.poets, (poet: Poet) => {
GridItem() {
Column() {
// 头像
Image($rawfile(poet.avatar))
.width(56)
.height(56)
.borderRadius(28)
.objectFit(ImageFit.Cover)
Text(poet.name)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.margin({ top: 6 })
Text(poet.dynasty)
.fontSize(11)
.fontColor('#8B7355')
Text(`${poet.poemCount}首`)
.fontSize(10)
.fontColor('#ccc')
.margin({ top: 2 })
Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.Center }) {
ForEach(poet.styleTags, (tag: string) => {
Text(tag)
.fontSize(9)
.fontColor('#8B4513')
.backgroundColor('#FFF0D0')
.borderRadius(6)
.padding({ left: 5, right: 5, top: 1, bottom: 1 })
.margin(2)
})
}
}
.width('100%')
.padding(12)
.backgroundColor(
this.selectedPoetId === poet.id ? '#FFF5E6' : '#FAFAFA'
)
.borderRadius(12)
.shadow({
radius: this.selectedPoetId === poet.id ? 8 : 4,
color: '#20000000',
offsetX: 0,
offsetY: 2
})
.onClick(() => this.selectPoet(poet.id))
}
})
}
.columnsTemplate('1fr 1fr')
.rowsTemplate('1fr 1fr 1fr 1fr')
.columnsGap(12)
.rowsGap(12)
.padding(12)
.height(380)
// ③ 分隔控制栏
Row() {
Row() {
Text(this.selectedPoet?.name || '')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#8B4513')
Text(` · ${this.filteredPoems.length}首`)
.fontSize(13)
.fontColor('#666')
Blank()
Button(this.isPlaying ? '⏹ 停止' : '▶ 朗读全部')
.fontSize(12)
.fontColor('#FFFFFF')
.backgroundColor(this.isPlaying ? '#D32F2F' : '#8B4513')
.borderRadius(16)
.height(32)
.onClick(() => {
this.isPlaying ? this.stopTts() : this.startTts();
})
}
.padding({ left: 16, right: 16 })
}
.width('100%')
.height(48)
.backgroundColor('#FFF8E7')
// ④ 诗词列表区
List({ space: 8 }) {
ForEach(this.filteredPoems, (poem: Poem, index: number) => {
ListItem() {
Row() {
// 序号
Text(`${index + 1}`)
.fontSize(14)
.fontColor('#8B4513')
.fontWeight(FontWeight.Bold)
.width(28)
.textAlign(TextAlign.Center)
Column() {
Row() {
Text(poem.title)
.fontSize(16)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.layoutWeight(1)
Text(poem.genre)
.fontSize(10)
.fontColor('#8B4513')
.backgroundColor('#FFF0D0')
.borderRadius(6)
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
}
Text(poem.background)
.fontSize(11)
.fontColor('#999')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.margin({ top: 2 })
Text(poem.content.length > 26 ?
poem.content.substring(0, 24) + '···' :
poem.content)
.fontSize(12)
.fontColor('#666')
.italic(true)
.margin({ top: 2 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
// 收藏
Image($rawfile(poem.isFavorite ? 'star_filled.png' : 'star_outline.png'))
.width(20)
.height(20)
.onClick(() => this.toggleFavorite(poem.id))
}
.width('100%')
.padding(12)
.backgroundColor('#FAFAFA')
.borderRadius(10)
.onClick(() => this.openPoemDetail(poem))
}
})
}
.width('100%')
.layoutWeight(1)
.padding({ left: 12, right: 12, bottom: 12 })
}
.width('100%')
.height('100%')
.backgroundColor('#F8F6F2')
}
}
八、主题样式与视觉设计
8.1 配色方案
诗词鉴赏应用采用"素雅古风"配色方案,体现传统文化韵味:
| 用途 | 色值(HEX) | 色值(RGBA) | 说明 |
|---|---|---|---|
| 主色 | #8B4513 | rgb(139,69,19) | 赭石色,古书墨韵 |
| 背景 | #F8F6F2 | rgb(248,246,242) | 宣纸色,柔和护眼 |
| 卡片 | #FAFAFA | rgba(250,250,250) | 近白色,干净通透 |
| 选中 | #FFF5E6 | rgba(255,245,230) | 浅杏色,温和高亮 |
| 标签底 | #FFF0D0 | rgba(255,240,208) | 淡金色,标签背景 |
| 正文 | #333333 | rgba(51,51,51) | 深灰色,阅读舒适 |
| 辅助 | #999999 | rgba(153,153,153) | 灰色,次要信息 |
| 强调 | #D32F2F | rgba(211,47,47) | 红色,停止/警告 |
8.2 字体与排版
- 标题:使用
fontWeight: FontWeight.Bold,字号 16-20vp,行高 1.6 - 正文(诗词原文):字号 16vp,行高 28vp(约1.75倍),字间距 1vp,营造古书竖排的呼吸感
- 辅助文字:字号 11-13vp,颜色 #999/#666,用于背景、体裁标签等次要信息
- 圆角规范:卡片 12vp,小标签 6vp,按钮 16vp(药丸形)
8.3 图片资源
用户需准备以下资源文件,放置在 entry/src/main/resources/rawfile/ 目录下:
rawfile/
├── images/poets/
│ ├── libai.png
│ ├── dufu.png
│ ├── sushi.png
│ ├── liqingzhao.png
│ ├── baijuyi.png
│ ├── wangwei.png
│ ├── lishangyin.png
│ └── xinqiji.png
├── icon_back.png // 返回箭头
├── icon_tts.png // TTS朗读
├── star_filled.png // 已收藏(实心星)
└── star_outline.png // 未收藏(空心星)
九、性能优化与最佳实践
9.1 List的性能优化
对于诗词列表(最多6-8项),性能压力不大,但作为示范需要遵循以下原则:
.lazy属性:当数据量超过50条时,为List启用懒加载模式- ListItem复用:ArkTS Runtime会自动复用ListItem,但要注意避免在
ForEach中创建复杂的匿名函数 - 固定高度:如果列表项高度一致,使用
.estimatedHeight(80)提前告知布局引擎
9.2 Grid的性能优化
.cachedCount:设置cachedCount(2)预渲染上下各2行,减少滚动时的白屏- 减少GridItem内部嵌套层级:单层Column够用就不要用多层Flex嵌套
- 图片资源:诗人头像控制在 60×60px,使用
objectFit(ImageFit.Cover)而非ImageFit.Fill避免形变
9.3 TTS性能考虑
- 引擎复用:整个应用生命周期内只创建一次TTS Engine,不要每次朗读都重新初始化
- 异步非阻塞:
speak()方法是异步的,使用await确保顺序执行但不要阻塞UI线程 - 内存释放:在页面
aboutToDisappear中务必调用release()释放音频资源
9.4 状态管理原则
- 最小化 @State:只将用户交互直接改变的变量声明为
@State,派生数据用get方法 - 深拷贝触发刷新:修改对象属性后,使用展开运算符重新赋值(
this.poems = [...this.poems])强制刷新 - 避免不必要的ForEach key变更:为每个列表项提供稳定的
key(如poem.id.toString())
十、测试与验证
10.1 功能测试用例
| 测试项 | 操作步骤 | 预期结果 |
|---|---|---|
| 诗人切换 | 点击不同诗人卡片 | 下方列表切换为该诗人的作品 |
| 诗词详情 | 点击列表项 | 跳转到详情页,展示原文 |
| TTS朗读 | 点击"朗读全部" | 按顺序朗读当前诗人的所有作品 |
| TTS停止 | 点击"停止" | 立即停止朗读 |
| 收藏切换 | 点击收藏图标 | 图标在实心和空心之间切换 |
| Tab切换 | 在详情页点击不同Tab | 显示对应的注释/翻译/赏析内容 |
10.2 兼容性验证
| 设备类型 | 分辨率 | 布局预期 |
|---|---|---|
| Phone(6.1寸) | 360×780 | 2列Grid正常显示 |
| Phone(6.7寸) | 393×852 | 2列Grid居中均匀分布 |
| Tablet(10寸) | 1280×800 | 2列Grid可以保留,或改为3列 |
| Foldable(展开) | 900×1200 | Grid自动适配空间 |
十一、总结与扩展方向
11.1 本文要点回顾
通过构建"鸿蒙ArkTS诗词集"应用,我们完整实现了以下关键技术:
- Grid布局:通过
columnsTemplate实现2列自适应网格,展示诗人卡片 - List布局:通过
List + ListItem实现纵向滚动列表,展示诗词条目 - Grid-List联动:通过
@State和 computed getter 实现选中诗人后列表实时过滤 - TTS语音播报:调用
@ohos.multimedia.textToSpeech实现诗词的语音朗读 - 路由导航:使用
@ohos.router实现主页面到详情页的跳转与参数传递
11.2 可扩展的功能方向
- 全文搜索:利用
Search组件集成诗词标题和诗句的模糊搜索 - 随机一诗:增加"每日一诗"功能,每天推荐一首随机诗词
- 多语言翻译:支持英文/日文翻译,推动中华文化出海
- 水墨动画:结合Canvas API实现水墨风格的动画背景
- 离线词库:扩展至《全唐诗》四万余首,使用SQLite本地存储
- 社区功能:用户可发表对诗词的感悟点评,形成诗词爱好者社区
- AI辅助赏析:集成AI大模型,为用户提供个性化的诗词解读
11.3 对ArkTS开发者的建议
对于正在学习ArkTS的开发者,本文演示的List+Grid双布局模式是一个非常实用的"万能模板":
- 电商应用:Grid展示商品分类 → List展示商品详情
- 社交应用:Grid展示用户分类 → List展示动态内容
- 音乐应用:Grid展示专辑封面 → List展示歌曲列表
- 学习应用:Grid展示科目分类 → List展示课程章节
这种"概览-详情"的双层信息架构,几乎适用于所有内容型应用,建议开发者将其作为ArkTS开发的"基础设计模式"之一熟练掌握。
十二、ArkTS开发哲学:声明式UI的思考
12.1 从命令式到声明式的思维转变
对于从Java或传统Android开发转过来的开发者而言,ArkTS最大的挑战不在语法层面,而在思维方式本身。在传统的命令式UI开发中,开发者需要亲自维护UI的每一个状态变化:button.setText("已收藏")、listView.notifyDataSetChanged()、gridView.setVisibility(View.GONE)——每一步变化都需要明确的指令。而在ArkTS的声明式体系中,开发者不再描述"如何变化",而是描述"应该是什么样子",框架自动计算变化路径。
这种思维的转变可以用一个比喻来理解:命令式编程像一名详细的导演,事无巨细地告诉每个演员每一步应该怎么走;声明式编程像一名建筑设计师,画出最终的设计图,由施工团队(ArkTS框架)去决定最佳的实现路径。对于诗词集应用而言,这意味着:
// 命令式思维(伪代码)
user clicks poet card →
find poet by id →
set selectedPoetId variable →
clear list →
query poems by poet id →
rebuild list items →
update TTS bar text →
scroll list to top
// 声明式思维(ArkTS实际代码)
@State selectedPoetId: number = 1;
// 框架自动完成:诗人类别高亮变化、列表过滤、控制栏文本更新
开发者只需要声明 selectedPoetId 是一个状态变量,然后在模板中描述"诗人卡片在 selectedPoetId === poet.id 时显示高亮"、"列表只展示 poetId === selectedPoetId 的诗词"即可。所有的派生变化由ArkTS的响应式系统自动处理。
12.2 分层架构的必然性
在编写ArkTS应用时,很多初学者容易犯的一个错误是将所有逻辑塞入页面文件(.ets),导致单个文件达到上千行。我们的诗词集应用虽然以 index.ets 为演示核心,但在真实工程中必须遵循分层架构:
| 层级 | 职责 | 文件示例 | 与UI关系 |
|---|---|---|---|
| 页面层 | 布局与交互 | index.ets | 直接渲染UI |
| 数据层 | 数据加载与缓存 | PoemModel.ets | 页面读取数据 |
| 服务层 | 业务逻辑与系统API | TtsService.ets | 页面调用服务 |
| 公共层 | 常量与工具函数 | constants.ets | 各层引用 |
这种分层带来了三个核心好处:
- 可测试性:数据模型可以脱离UI单独进行单元测试。例如,测试
PoemModel.getAllPoems()是否返回了正确数量的诗词,不需要启动模拟器 - 可复用性:
TtsService既可以在主页面用于批量朗读,也可以在详情页用于单首朗读,只需一处实现两处调用 - 可维护性:当华为更新TTS的API时,只需修改
TtsService.ets一个文件,所有调用方无需改动
12.3 性能意识:响应式系统的隐性契约
ArkTS的响应式系统虽然强大,但它建立在一个隐性契约之上:开发者需要理解"什么变化会触发刷新,什么不会"。不遵循这个契约会导致两种典型的性能问题:
问题一:不必要的全量刷新
// ❌ 错误做法:每次选诗人时重新创建整个诗词数组
selectPoetBad(id: number) {
this.selectedPoetId = id;
this.displayedPoems = this.allPoems.filter(p => p.poetId === id);
// 这会触发 List 的完全重建
}
// ✅ 正确做法:使用 computed getter 而非独立状态
get displayedPoems(): Poem[] {
return this.allPoems.filter(p => p.poetId === this.selectedPoetId);
// 只刷新 affected UI 节点
}
问题二:对象引用不变导致的刷新失效
// ❌ 错误做法:直接修改对象属性,引用不变
toggleFavorite(id: number) {
const poem = this.poems.find(p => p.id === id);
if (poem) poem.isFavorite = !poem.isFavorite;
// UI 不会刷新!因为 poems 数组的引用没有变化
}
// ✅ 正确做法:创建新数组触发引用变化
toggleFavorite(id: number) {
this.poems = this.poems.map(p => {
if (p.id === id) return { ...p, isFavorite: !p.isFavorite };
return p;
});
}
这个隐形契约的根源在于JavaScript/TypeScript的对象引用机制。ArkTS使用引用比较来判断状态是否变化,直接修改对象的属性不会改变引用地址,因此UI无法感知变化。使用展开运算符(...)创建新对象是一个简单而有效的解决方案。
12.4 文化自信与技术自信
写到这里,我们不妨将视野拉远一些。HarmonyOS的诞生背景是全球技术格局的深刻变革,而ArkTS作为鸿蒙生态的核心开发语言,承载着中国基础软件自主创新的使命。当我们用ArkTS开发一款诗词鉴赏应用时,这不仅仅是技术实现,更是中国开发者在自主技术栈上传承中华优秀传统文化的具体实践。
从某种意义上说,Grid网格中的每一位诗人——李白、杜甫、苏轼、李清照——他们都是千年前那个时代的"创新者"。李白打破了格律的束缚创造了全新的诗歌境界,苏轼在词的领域拓展了前所未有的表现空间。今天的鸿蒙开发者,同样在创造一个新的数字世界——一个基于中国自主操作系统、面向万物互联时代的新世界。这种跨越千年的精神呼应,正是"诗词+代码"这个组合最动人的地方。
12.5 给学习者的最后建议
如果你是一位正在学习ArkTS的开发者,以下三点建议或许能帮你更快地成长:
-
从模仿开始,但不要止于模仿:跟随本文的代码实现一遍诗词集应用后,尝试将其改造为另一个主题——比如"书法鉴赏"“民乐百科”“戏曲文化”——保持List+Grid的架构不变,替换数据源和视觉风格。只有通过改造,你才能真正理解架构的边界在哪里。
-
养成阅读官方文档的习惯:华为开发者文档的质量在持续提升,API参考、开发指南、Sample代码都在不断完善。遇到问题时,优先查官方文档而非搜索引擎——你很可能在官方文档中找到最佳实践。
-
关注 HarmonyOS 生态的演进:ArkTS在快速迭代中,每年有两次大版本更新(秋季和春季),每次都会引入大量新特性和性能优化。关注华为开发者大会(HDC)、订阅官方技术博客、加入鸿蒙开发者社区,这些投入的回报会远超你的预期。
最后,送给大家一句苏轼的话作为本文的结尾:"古之立大事者,不惟有超世之才,亦必有坚忍不拔之志。"学习一门新的开发语言、构建一个完整的鸿蒙应用,确实需要坚忍不拔之志。但当你的第一个ArkTS应用在真机上流畅运行时,那份成就感是无法用语言形容的。
附录:完整参考文献
-
华为开发者文档 - TextToSpeech开发指导
https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/texttospeech-guide-V5 -
华为开发者文档 - ArkTS List组件
https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V5/ts-container-list-V5 -
华为开发者文档 - ArkTS Grid组件
https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V5/ts-container-grid-V5 -
华为开发者文档 - 状态管理
https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/arkts-state-management-V5 -
《唐诗三百首》中华书局
-
《宋词三百首》中华书局
更多推荐



所有评论(0)