# 鸿蒙 HarmonyOS 应用开发实战(第36期)|阅读清单(Reading List)— 三态流转的书单管理器
一、应用概述
阅读清单(Reading List) 是一款面向书籍爱好者的清单管理工具。用户可以添加想读的书籍(书名+作者),并通过点击状态标签在「想读 → 在读 → 已读」三个状态之间循环切换。同时支持按状态筛选查看和删除书籍。
对于经常读书的人来说,管理阅读进度是一个常见需求。这款应用以极简的方式实现了书单的三态管理,是 ArkTS 中状态流转、条件筛选和列表操作的经典案例。从技术层面,它展示了 Text 作为交互按钮的巧妙用法,以及 concat() 不可变更新在增、改、删三种操作中的一致应用。
二、技术架构与组件树
2.1 整体架构
@Entry @Component struct index36
├── Column (根容器)
│ ├── Row (顶部导航栏)
│ ├── Row (添加书籍输入区)
│ │ ├── TextInput (书名, layoutWeight=1)
│ │ ├── TextInput (作者, layoutWeight=1)
│ │ └── Button ("+" 添加)
│ ├── Row (状态过滤器)
│ │ └── ForEach → Text × 4 (全部/想读/在读/已读)
│ └── List (书籍列表, layoutWeight=1)
│ └── ForEach → ListItem → Row (书籍卡片)
│ ├── Column
│ │ ├── Text (书名)
│ │ └── Text (作者)
│ ├── Text (状态标签, 可点击切换)
│ └── Text ("✕" 删除)
2.2 数据模型
interface Book {
id: number;
title: string;
author: string;
status: string; // "想读" | "在读" | "已读"
}
@State books: Book[] = [
{ id: 1, title: '活着', author: '余华', status: '想读' },
{ id: 2, title: '三体', author: '刘慈欣', status: '在读' },
{ id: 3, title: '百年孤独', author: '加西亚·马尔克斯', status: '已读' },
];
@State title: string = '';
@State author: string = '';
@State filter: string = '全部';
nextId: number = 4;
三个初始书籍示例涵盖了三种状态,用户一打开应用就能理解使用方式。nextId 作为自增 ID 生成器,从 4 开始(因为已有 3 本书占用了 1~3)。
三、核心代码深度解析
3.1 添加书籍——不可变数组追加
addBook(): void {
if (this.title.trim() === '' || this.author.trim() === '') return;
const newBooks: Book[] = this.books.concat();
newBooks.push({ id: this.nextId++, title: this.title,
author: this.author, status: '想读' });
this.books = newBooks;
this.title = '';
this.author = '';
}
三步操作:
- 校验:检查书名和作者是否为空(
trim()清除首尾空格后判断) - 不可变追加:
concat()创建副本 →push()添加新书 → 重新赋值 - 清空输入:添加后自动清空输入框,提升连续添加的体验
新书默认状态为「想读」,符合直觉——刚加入书单时自然是打算读的。
3.2 状态流转——三段式循环切换
changeStatus(id: number): void {
const newBooks: Book[] = this.books.concat();
for (let i = 0; i < newBooks.length; i++) {
if (newBooks[i].id === id) {
const cur = newBooks[i].status;
let next = '想读';
if (cur === '想读') next = '在读';
else if (cur === '在读') next = '已读';
else if (cur === '已读') next = '想读';
newBooks[i] = { id: newBooks[i].id, title: newBooks[i].title,
author: newBooks[i].author, status: next };
}
}
this.books = newBooks;
}
三态流转图:
想读 ──→ 在读 ──→ 已读
↑ │
└──────────────────┘
每个状态点击后顺时针切换到下一个状态:
- 「想读」→「在读」(开始阅读了)
- 「在读」→「已读」(读完了)
- 「已读」→「想读」(想重新读,或标记错误需要修正)
这种循环设计比独立的「状态选择器」更简洁——用户只需反复点击即可到达任何状态,无需弹出菜单或选择器。
注意更新时使用了对象解构重建了一个新对象({ id, title, author, status: next }),而不是直接修改 newBooks[i].status。这是因为 interface Book 定义了不可变结构,虽然直接修改属性在 JS 层面可行,但保持整体替换的模式更一致、更安全。
3.3 删除书籍——不可变数组过滤
deleteBook(id: number): void {
const newBooks: Book[] = [];
for (let i = 0; i < this.books.length; i++) {
if (this.books[i].id !== id) {
newBooks.push(this.books[i]);
}
}
this.books = newBooks;
}
删除操作在不使用 filter() 高阶函数的情况下,手动实现了过滤逻辑。遍历原数组,将 ID 不匹配的元素加入新数组,实现"排除"的效果。当然,使用 this.books.filter(book => book.id !== id) 是更简洁的写法,但手动循环更易于理解底层原理。
3.4 状态筛选
get filteredBooks(): Book[] {
if (this.filter === '全部') return this.books;
const result: Book[] = [];
for (let i = 0; i < this.books.length; i++) {
if (this.books[i].status === this.filter) {
result.push(this.books[i]);
}
}
return result;
}
筛选逻辑很简单:「全部」不过滤,否则只返回状态匹配的书籍。这个计算属性在 build() 中被 List 使用,当 filter 或 books 变化时自动重新计算并刷新列表。
3.5 状态标签的视觉设计
getStatusColor(s: string): string {
if (s === '想读') return '#FF9800';
if (s === '在读') return '#0077FF';
return '#4CAF50';
}
三种状态使用三种颜色编码:
- 想读(橙色 #FF9800):代表期待和热情
- 在读(蓝色 #0077FF):代表进行中的专注
- 已读(绿色 #4CAF50):代表完成的成就感
Text(book.status)
.fontSize(12).padding({ top: 4, bottom: 4, left: 10, right: 10 })
.backgroundColor(this.getStatusColor(book.status))
.fontColor('#fff').borderRadius(10)
.onClick(() => this.changeStatus(book.id))
状态标签本身是一个可点击的 Text,而不是 Button。点击触发 changeStatus,实现状态流转。这种「标签即按钮」的设计让 UI 更加紧凑,减少了额外的操作元素。
3.6 过滤器按钮组
Row({ space: 8 }) {
ForEach(['全部', '想读', '在读', '已读'], (f: string) => {
Text(f)
.fontSize(13).padding({ top: 4, bottom: 4, left: 12, right: 12 })
.backgroundColor(this.filter === f ? '#0077FF' : '#eee')
.fontColor(this.filter === f ? '#fff' : '#333')
.borderRadius(12).onClick(() => { this.filter = f })
})
}
过滤器按钮使用 ForEach 遍历四个字符串数组生成。选中状态通过背景色和文字颜色的变化来区分:
- 选中:蓝色背景 + 白色文字
- 未选中:灰色背景 + 深色文字
这种胶囊按钮的样式在 iOS 和 Material Design 中都很常见,视觉效果简洁明快。
四、HarmonyOS 特性分析
4.1 List 组件的 key 生成
ForEach(this.filteredBooks, (book: Book) => {
ListItem() { ... }
}, (book: Book) => book.id.toString())
本例为 ForEach 提供了第三个参数——key 生成函数。book.id.toString() 为每本书生成了唯一标识。这非常重要:
- 有 key:框架知道哪个 item 是新增/删除/修改的,只更新发生变化的 ListItem
- 无 key:框架可能重建整个列表,性能下降且可能丢失焦点/滚动位置
在列表操作(增、删、改)的场景中,始终提供稳定的 key。
4.2 TextInput 的 onSubmit 事件
TextInput({ placeholder: '添加商品...', text: this.inputText })
.onSubmit(() => this.addItem())
虽然本例在阅读清单中没有使用 onSubmit 事件(购物清单中使用了),但值得提及——TextInput 的 onSubmit 在用户点击键盘上的「确认/完成」键时触发。这让用户可以直接按键盘确认添加,无需手动点击「+」按钮,提升输入效率。
4.3 layoutWeight 实现弹性输入框
TextInput({ placeholder: '书名', text: this.title })
.layoutWeight(1).height(40)
TextInput({ placeholder: '作者', text: this.author })
.layoutWeight(1).height(40)
两个 TextInput 都设置了 layoutWeight(1),它们将等分 Row 容器中除「+」按钮外的所有空间。这种弹性布局让输入框在不同屏幕宽度下都能合理分配空间。
五、UI/UX 设计与最佳实践
5.1 微交互设计
- 点击状态标签流转:状态标签同时是状态显示和操作入口,一个点击完成状态切换
- 添加后自动清空:添加书籍后输入框清空,便于连续添加
- 红色删除按钮:「✕」使用红色 (#F44336),符合"删除=危险"的通用设计惯例
5.2 筛选器位置
筛选器位于输入区和列表之间,形成了「输入 → 筛选 → 浏览」的自然操作流。用户先添加书籍,然后可以按状态筛选查看。
5.3 空状态处理
当前版本没有处理 filter 筛选后无匹配项的情况。最佳实践是添加一个空状态提示:
if (this.filteredBooks.length === 0) {
Text('暂无匹配的书籍').fontSize(14).fontColor('#aaa')
}
5.4 数据持久化的缺失
当前所有数据保存在内存中,页面销毁后数据丢失。对于阅读清单这种需要长期使用的应用,数据持久化是必需的。
六、可扩展思路
- 数据持久化:使用
@ohos.data.preferences或关系型数据库(RDB)保存书籍列表。 - 添加/编辑功能增强:支持修改书名、作者,添加书籍封面、评分、阅读笔记。
- 排序功能:按添加时间、书名、状态排序。
- 搜索功能:按书名或作者关键词搜索书籍。
- 云同步:通过云端服务在不同设备间同步书单。
七、总结
阅读清单应用以 130 行代码,实现了一个三态流转的书籍管理器。它的技术要点包括:
- Interface 数据建模:定义 Book 接口确保数据结构一致性
- 不可变数组操作:增(concat + push)、改(concat + 替换元素)、删(过滤构建新数组)
- 三态循环切换:if/else if 链实现状态机流转
- ForEach key 优化:为列表提供稳定的 key 以提升渲染性能
- 条件筛选:通过计算属性实现按状态过滤
- 标签即按钮:Text 组件作为交互元素,减少 UI 冗余
这个应用的核心代码模式——数组状态 + 不可变更新 + ForEach 列表渲染——是 ArkTS 中 CRUD 类应用的基础架构,可以广泛复用到待办事项、收藏夹、联系人管理等场景。
下一期我们将继续清单类应用的主题,探讨一个更贴近日常生活的工具——购物清单(Shopping List),看如何加入勾选标记和进度条功能。
本文是 HarmonyOS 应用开发实战系列第 36 期,更多内容请关注专栏。
更多推荐



所有评论(0)