HarmonyOS APP<玩转React>开源教程二十:收藏功能实现
第20次:收藏功能实现
收藏功能是学习类应用里非常高频、但又很容易被写散的模块。当前项目把这套能力拆成了
BookmarkService.ets和BookmarkPage.ets两部分,再由LessonDetail、ModuleDetail等页面接入。我们这一篇就按真实结构把它讲清楚。
先看收藏功能的闭环
收藏不是只有一个“点星星”的按钮,它其实包含了完整链路:
- 某一节课被用户收藏
- 收藏状态写入本地存储
- 页面即时刷新星标状态
- 用户进入“我的收藏”页浏览收藏列表
- 再从收藏列表跳回对应课程
一、为什么收藏要单独做服务层
很多初学者会在页面里直接维护一个 isBookmarked 布尔值,但那只能解决当前页面显示问题,解决不了:
- 关闭应用后收藏是否还在
- 模块详情页和课时详情页能不能共享同一状态
- 收藏页能不能统一展示所有收藏记录
所以当前项目专门做了 BookmarkService,把下面这些职责统一收口:
- 加载收藏
- 保存收藏
- 添加收藏
- 取消收藏
- 判断是否已收藏
- 获取收藏数量
- 清空全部收藏
这就是很典型的“页面只发动作,服务负责状态和持久化”。
二、收藏数据模型为什么只存两个 ID
当前收藏记录 Bookmark 主要保存:
lessonIdmoduleIdaddedAt
注意,它并没有把课程标题、模块标题整份冗余进去。这是一个很好的设计选择,因为标题和模块信息本来就属于教程数据,不应该在收藏记录里重复存一份。
这样设计的好处是:
- 存储更轻
- 不容易产生冗余数据
- 如果课程标题未来更新,收藏页读取的还是最新标题
也就是说,收藏记录只保存“定位信息”,真正展示时再去 TutorialService 查回标题和模块名。
三、toggleBookmark() 为什么是核心方法
当前服务层最实用的方法就是 toggleBookmark(lessonId, moduleId)。页面不需要先自己判断“我该加还是该删”,而是直接把动作交给服务层。
它内部流程大致是:
- 通过
isBookmarked(lessonId)判断当前状态。 - 如果已收藏,就执行
removeBookmark。 - 如果未收藏,就执行
addBookmark。 - 返回切换后的布尔结果。
这能让页面层代码非常简洁。比如在 LessonDetail 中,只需要:
- 点击收藏按钮
- 调用
toggleBookmark - 用返回值更新本地
isBookmarked
页面逻辑清楚了,Bug 也会少很多。
四、为什么还要做缓存
你会发现 BookmarkService 里维护了一个 cachedBookmarks。这意味着页面不用每次判断收藏时都去读磁盘。
这个缓存有两个实际价值:
ModuleDetail的课时列表渲染时,可以同步调用BookmarkService.isBookmarked(lesson.id)。LessonDetail顶部星标切换时,可以更快拿到当前状态。
如果没有缓存,你在列表页里每一项都异步读取一次收藏,会很难看,也会让页面逻辑复杂很多。
五、收藏页 BookmarkPage 做了什么
收藏页本身并不复杂,但职责很清晰:
- 页面出现时调用
loadBookmarks() - 空状态时展示引导文案
- 有收藏时渲染收藏列表
- 每个收藏项点击后进入
LessonDetail - 点击星标可移除收藏
这里最值得你注意的是两个辅助方法:
getLessonTitle(bookmark)getModuleTitle(bookmark)
它们都不是直接从收藏记录里取标题,而是通过 TutorialService 根据 ID 去查最新数据。这就是上一节提到的“收藏记录只存定位信息”的直接体现。
六、模块详情页为什么还要有 bookmarkVersion
虽然收藏页自己会在每次进入时重新加载,但 ModuleDetail 这种列表页需要更及时的局部刷新,所以它用了 bookmarkVersion 做强制刷新。
这说明一件事:收藏系统虽然只有一个服务层,但不同页面接入它的方式可以不同。
LessonDetail:单课时场景,直接更新本地状态即可。ModuleDetail:列表场景,用版本号触发列表刷新更稳。BookmarkPage:独立收藏中心,重新读取收藏列表即可。
所以你不要把“接入同一个服务”理解成“每个页面写法必须完全一样”。真正成熟的做法是:服务统一,页面根据自己的结构决定刷新方式。
七、自己实操时,建议这样验证
- 从模块详情页进入一节课。
- 在课时详情页点击星标,确认收藏状态变化。
- 返回模块详情页,看对应课时的收藏状态是否同步更新。
- 打开“我的收藏”页,确认刚才那节课已经出现。
- 在收藏页点击该项,确认能再次进入课程详情。
- 再点击右侧星标移除,确认列表会即时更新。
这套流程一旦跑通,你对收藏系统的认识就会非常完整。
八、本篇常见坑
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 }
});
})
按这个顺序动手
- 打开
entry/src/main/ets/services/BookmarkService.ets。 - 先读
addBookmark、removeBookmark、toggleBookmark三个方法。 - 再打开
BookmarkPage.ets,观察收藏记录如何通过moduleId + lessonId回跳课程页。
课后练习
- 思考如果你要给收藏页增加“按模块分组显示”,可以直接复用
BookmarkService的哪个方法。 - 说明为什么收藏记录里保留
addedAt是有价值的。 - 观察
BookmarkPage没有显示收藏时间,如果要加,你会放在条目的什么位置更合适。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)