第20次:收藏功能实现

收藏功能是学习类应用里非常高频、但又很容易被写散的模块。当前项目把这套能力拆成了 BookmarkService.etsBookmarkPage.ets 两部分,再由 LessonDetailModuleDetail 等页面接入。我们这一篇就按真实结构把它讲清楚。


先看收藏功能的闭环

收藏不是只有一个“点星星”的按钮,它其实包含了完整链路:

  • 某一节课被用户收藏
  • 收藏状态写入本地存储
  • 页面即时刷新星标状态
  • 用户进入“我的收藏”页浏览收藏列表
  • 再从收藏列表跳回对应课程

LessonDetail / ModuleDetail 点击收藏

BookmarkService.toggleBookmark

loadBookmarks

判断当前是否已收藏

addBookmark / removeBookmark

saveBookmarks 写回本地

BookmarkPage.loadBookmarks

收藏列表渲染并支持跳回课程详情


一、为什么收藏要单独做服务层

很多初学者会在页面里直接维护一个 isBookmarked 布尔值,但那只能解决当前页面显示问题,解决不了:

  • 关闭应用后收藏是否还在
  • 模块详情页和课时详情页能不能共享同一状态
  • 收藏页能不能统一展示所有收藏记录

所以当前项目专门做了 BookmarkService,把下面这些职责统一收口:

  • 加载收藏
  • 保存收藏
  • 添加收藏
  • 取消收藏
  • 判断是否已收藏
  • 获取收藏数量
  • 清空全部收藏

这就是很典型的“页面只发动作,服务负责状态和持久化”。


二、收藏数据模型为什么只存两个 ID

当前收藏记录 Bookmark 主要保存:

  • lessonId
  • moduleId
  • addedAt

注意,它并没有把课程标题、模块标题整份冗余进去。这是一个很好的设计选择,因为标题和模块信息本来就属于教程数据,不应该在收藏记录里重复存一份。

这样设计的好处是:

  • 存储更轻
  • 不容易产生冗余数据
  • 如果课程标题未来更新,收藏页读取的还是最新标题

也就是说,收藏记录只保存“定位信息”,真正展示时再去 TutorialService 查回标题和模块名。


三、toggleBookmark() 为什么是核心方法

当前服务层最实用的方法就是 toggleBookmark(lessonId, moduleId)。页面不需要先自己判断“我该加还是该删”,而是直接把动作交给服务层。

它内部流程大致是:

  1. 通过 isBookmarked(lessonId) 判断当前状态。
  2. 如果已收藏,就执行 removeBookmark
  3. 如果未收藏,就执行 addBookmark
  4. 返回切换后的布尔结果。

这能让页面层代码非常简洁。比如在 LessonDetail 中,只需要:

  • 点击收藏按钮
  • 调用 toggleBookmark
  • 用返回值更新本地 isBookmarked

页面逻辑清楚了,Bug 也会少很多。


四、为什么还要做缓存

你会发现 BookmarkService 里维护了一个 cachedBookmarks。这意味着页面不用每次判断收藏时都去读磁盘。

这个缓存有两个实际价值:

  1. ModuleDetail 的课时列表渲染时,可以同步调用 BookmarkService.isBookmarked(lesson.id)
  2. LessonDetail 顶部星标切换时,可以更快拿到当前状态。

如果没有缓存,你在列表页里每一项都异步读取一次收藏,会很难看,也会让页面逻辑复杂很多。


五、收藏页 BookmarkPage 做了什么

收藏页本身并不复杂,但职责很清晰:

  • 页面出现时调用 loadBookmarks()
  • 空状态时展示引导文案
  • 有收藏时渲染收藏列表
  • 每个收藏项点击后进入 LessonDetail
  • 点击星标可移除收藏

这里最值得你注意的是两个辅助方法:

  • getLessonTitle(bookmark)
  • getModuleTitle(bookmark)

它们都不是直接从收藏记录里取标题,而是通过 TutorialService 根据 ID 去查最新数据。这就是上一节提到的“收藏记录只存定位信息”的直接体现。


六、模块详情页为什么还要有 bookmarkVersion

虽然收藏页自己会在每次进入时重新加载,但 ModuleDetail 这种列表页需要更及时的局部刷新,所以它用了 bookmarkVersion 做强制刷新。

这说明一件事:收藏系统虽然只有一个服务层,但不同页面接入它的方式可以不同。

  • LessonDetail:单课时场景,直接更新本地状态即可。
  • ModuleDetail:列表场景,用版本号触发列表刷新更稳。
  • BookmarkPage:独立收藏中心,重新读取收藏列表即可。

所以你不要把“接入同一个服务”理解成“每个页面写法必须完全一样”。真正成熟的做法是:服务统一,页面根据自己的结构决定刷新方式。


七、自己实操时,建议这样验证

  1. 从模块详情页进入一节课。
  2. 在课时详情页点击星标,确认收藏状态变化。
  3. 返回模块详情页,看对应课时的收藏状态是否同步更新。
  4. 打开“我的收藏”页,确认刚才那节课已经出现。
  5. 在收藏页点击该项,确认能再次进入课程详情。
  6. 再点击右侧星标移除,确认列表会即时更新。

这套流程一旦跑通,你对收藏系统的认识就会非常完整。


八、本篇常见坑

1. 把课程标题直接写进收藏记录

这样会造成数据冗余,也不利于后期课程数据更新。

2. 页面自己判断“加收藏还是取消收藏”

更好的写法是统一交给 toggleBookmark()

3. 忽略缓存

没有缓存的话,列表页判断收藏状态会非常笨重。

4. 收藏页只展示 ID,不回查标题

这样用户体验会很差,也说明数据设计没有分层好。


本篇小结

这一篇最值得记住的不是某个按钮怎么写,而是收藏系统的设计方式:

  • 收藏记录只存定位信息
  • 服务层统一处理增删改查
  • 页面层根据场景选择最合适的刷新策略

理解这个思路后,你会发现错题本、历史记录、最近浏览这些功能,本质上都可以照着类似方式去做。


跟着真实源码继续往下看

BookmarkService 里最关键的方法就是下面这段:

static async toggleBookmark(lessonId: string, moduleId: string): Promise<boolean> {
  const isCurrentlyBookmarked = BookmarkService.isBookmarked(lessonId);

  if (isCurrentlyBookmarked) {
    await BookmarkService.removeBookmark(lessonId);
    return false;
  } else {
    await BookmarkService.addBookmark(lessonId, moduleId);
    return true;
  }
}

收藏页里跳回课程详情的真实代码如下:

.onClick(() => {
  router.pushUrl({
    url: 'pages/LessonDetail',
    params: { moduleId: bookmark.moduleId, lessonId: bookmark.lessonId }
  });
})

按这个顺序动手

  1. 打开 entry/src/main/ets/services/BookmarkService.ets
  2. 先读 addBookmarkremoveBookmarktoggleBookmark 三个方法。
  3. 再打开 BookmarkPage.ets,观察收藏记录如何通过 moduleId + lessonId 回跳课程页。

课后练习

  1. 思考如果你要给收藏页增加“按模块分组显示”,可以直接复用 BookmarkService 的哪个方法。
  2. 说明为什么收藏记录里保留 addedAt 是有价值的。
  3. 观察 BookmarkPage 没有显示收藏时间,如果要加,你会放在条目的什么位置更合适。
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐