一、应用概述

阅读清单(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 = '';
}

三步操作

  1. 校验:检查书名和作者是否为空(trim() 清除首尾空格后判断)
  2. 不可变追加concat() 创建副本 → push() 添加新书 → 重新赋值
  3. 清空输入:添加后自动清空输入框,提升连续添加的体验

新书默认状态为「想读」,符合直觉——刚加入书单时自然是打算读的。

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 使用,当 filterbooks 变化时自动重新计算并刷新列表。

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 事件(购物清单中使用了),但值得提及——TextInputonSubmit 在用户点击键盘上的「确认/完成」键时触发。这让用户可以直接按键盘确认添加,无需手动点击「+」按钮,提升输入效率。

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 微交互设计

  1. 点击状态标签流转:状态标签同时是状态显示和操作入口,一个点击完成状态切换
  2. 添加后自动清空:添加书籍后输入框清空,便于连续添加
  3. 红色删除按钮:「✕」使用红色 (#F44336),符合"删除=危险"的通用设计惯例

5.2 筛选器位置

筛选器位于输入区和列表之间,形成了「输入 → 筛选 → 浏览」的自然操作流。用户先添加书籍,然后可以按状态筛选查看。

5.3 空状态处理

当前版本没有处理 filter 筛选后无匹配项的情况。最佳实践是添加一个空状态提示:

if (this.filteredBooks.length === 0) {
  Text('暂无匹配的书籍').fontSize(14).fontColor('#aaa')
}

5.4 数据持久化的缺失

当前所有数据保存在内存中,页面销毁后数据丢失。对于阅读清单这种需要长期使用的应用,数据持久化是必需的。

六、可扩展思路

  1. 数据持久化:使用 @ohos.data.preferences 或关系型数据库(RDB)保存书籍列表。
  2. 添加/编辑功能增强:支持修改书名、作者,添加书籍封面、评分、阅读笔记。
  3. 排序功能:按添加时间、书名、状态排序。
  4. 搜索功能:按书名或作者关键词搜索书籍。
  5. 云同步:通过云端服务在不同设备间同步书单。

七、总结

阅读清单应用以 130 行代码,实现了一个三态流转的书籍管理器。它的技术要点包括:

  • Interface 数据建模:定义 Book 接口确保数据结构一致性
  • 不可变数组操作:增(concat + push)、改(concat + 替换元素)、删(过滤构建新数组)
  • 三态循环切换:if/else if 链实现状态机流转
  • ForEach key 优化:为列表提供稳定的 key 以提升渲染性能
  • 条件筛选:通过计算属性实现按状态过滤
  • 标签即按钮:Text 组件作为交互元素,减少 UI 冗余

这个应用的核心代码模式——数组状态 + 不可变更新 + ForEach 列表渲染——是 ArkTS 中 CRUD 类应用的基础架构,可以广泛复用到待办事项、收藏夹、联系人管理等场景。

下一期我们将继续清单类应用的主题,探讨一个更贴近日常生活的工具——购物清单(Shopping List),看如何加入勾选标记和进度条功能。


本文是 HarmonyOS 应用开发实战系列第 36 期,更多内容请关注专栏。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐