【细胞工坊|10】HarmonyOS ArkTS 收藏与笔记实战:把实验收藏和本地笔记写进 Preferences
2026/8/8 10:56:41 网站建设 项目流程

部分内容由AI辅助生成。

本文面向 HarmonyOS 5.0 及以上版本,基于细胞工坊项目真实源码展开,源码根目录为D:\huawei\one14-9。本文重点复核这些文件:

  • entry/src/main/ets/views/experiment/ExperimentSimPage.ets
  • entry/src/main/ets/views/mine/FavoritesPage.ets
  • entry/src/main/ets/views/mine/NotesPage.ets
  • entry/src/main/ets/components/NoteEditorDialog.ets
  • entry/src/main/ets/utils/DataStore.ets

先把边界讲清楚:当前源码实现的是实验收藏手动学习笔记。收藏保存的是实验expId,收藏列表再用getAllExperiments()把 ID 映射回实验卡片;笔记通过NoteEditorDialog手动输入标题、内容、分类,再保存到user_notes。源码没有实现知识详情收藏、笔记自动同步知识详情、云同步、账号体系、跨设备同步或富文本笔记。

1. 收藏和笔记最容易出错的地方不是 UI,而是边界

在学习类 HarmonyOS 应用里,“收藏”和“笔记”看起来只是两个入口,但实际会牵涉多个页面:实验模拟页要能切换收藏状态,我的收藏页要能重新加载列表,笔记页要能新增和删除,数据层要能把这些记录稳定保存下来。

如果边界没划清,常见问题会很快出现:

问题表现源码里的处理方式
收藏按钮状态和列表不一致实验页点了收藏,返回列表看不到FavoritesPageaboutToAppear/onPageShow调用reload()
收藏保存整个对象实验改名后收藏里还是旧内容只保存实验 ID,展示时映射最新实验定义
笔记输入为空仍保存列表出现空标题或空内容NoteEditorDialog在保存前trim()并拦截空值
删除只改 UI 不改本地重进页面后被删笔记又出现removeNote()更新状态后调用DataStore.saveNotes()
本地统计不同步我的页面收藏数量不变DataStore.notifyStatsChanged()更新 AppStorage 快照

这篇文章要解决的工程问题是:在一个 ArkTS 应用里,用轻量级 Preferences 实现本地收藏和笔记,页面状态和持久化状态保持一致,同时不夸大源码没有实现的同步能力。

2. DataStore:把 Preferences 包成统一入口

DataStore.ets是本地数据入口。它使用 HarmonyOS 数据管理里的 Preferences:

import { preferences } from '@kit.ArkData' import { common } from '@kit.AbilityKit' const PREF_NAME = 'bio_lab_app_data' export class DataStore { private static prefInstance: preferences.Preferences | null = null static async init(context: common.UIAbilityContext): Promise<void> { try { DataStore.prefInstance = await preferences.getPreferences(context, PREF_NAME) await DataStore.refreshStatsSnapshot() } catch (_) { DataStore.prefInstance = null } } }

这里的关键是把preferences.Preferences实例藏在DataStore内部。页面不直接调用preferences.getPreferences(),而是调用DataStore.loadFavorites()DataStore.saveNotes()这类业务方法。

这种做法有三个好处:

  • 页面不用知道 Preferences 文件名;
  • JSON 解析和异常兜底集中处理;
  • 后续如果从 Preferences 换成 RDB 或文件存储,页面改动范围更小。

当前数据量很小,收藏只是字符串 ID 数组,笔记也只是本地对象数组,Preferences 是合适选择。若后续笔记支持全文搜索、标签过滤、图片附件或大量记录,就应考虑关系型数据库或文件存储。

3. 通用读写:失败时返回默认值,避免页面崩溃

DataStore的基础读写方法都做了异常兜底:

static async putString(key: string, value: string): Promise<void> { if (!DataStore.prefInstance) return try { await DataStore.prefInstance.put(key, value) await DataStore.prefInstance.flush() DataStore.notifyStatsChanged(key, value) } catch (_) { } } static async getString(key: string, defaultValue: string = ''): Promise<string> { if (!DataStore.prefInstance) return defaultValue try { const value = await DataStore.prefInstance.get(key, defaultValue) return value as string } catch (_) { return defaultValue } }

收藏和笔记都依赖字符串 JSON。写入时flush()保证数据落盘;读取失败时返回默认值,页面可以继续显示空态。

这段代码的工程取舍也很明显:它没有把错误抛到 UI 层,也没有显示失败提示。对于当前轻量学习工具来说,这能保证页面稳定;如果是强一致的生产记录系统,就应把保存失败反馈给用户,并提供重试。

4. 收藏数据结构:只保存实验 ID,不保存实验对象

收藏方法非常明确:

static async saveFavorites(ids: string[]): Promise<void> { await DataStore.putString('favorite_experiments', JSON.stringify(ids)) } static async loadFavorites(): Promise<string[]> { const json = await DataStore.getString('favorite_experiments', '[]') try { return JSON.parse(json) as string[] } catch { return [] } }

它只保存string[]。这比保存完整实验对象更稳。

保存方式优点风险
保存完整实验对象列表渲染不需要再查模型实验名称、图标、分类更新后,本地旧对象会过期
保存实验 ID本地数据小,展示时使用最新模型如果模型里删除实验 ID,需要过滤不存在项

当前源码选择第二种方式。FavoritesPage.reload()会处理“ID 已不存在”的情况,只把能找到的实验推入列表。

5. ExperimentSimPage:收藏入口在实验模拟页

实验模拟页维护收藏状态:

@State isFavorite: boolean = false @State expId: string = 'microscope_observation'

页面出现时加载收藏状态:

aboutToAppear(): void { const params = router.getParams() as SimRouterParams | undefined if (params?.expId) { this.expId = params.expId } if (params?.expName) { this.title = params.expName } this.initExperiment() this.resetExperiment() this.loadFavoriteState() } private async loadFavoriteState(): Promise<void> { const ids = await DataStore.loadFavorites() this.isFavorite = ids.includes(this.expId) }

这里先读路由参数,再初始化实验,再加载收藏状态。顺序很关键:如果先加载收藏,再更新expId,按钮状态就会根据默认实验计算,导致进入其他实验时收藏图标不准确。

收藏切换逻辑如下:

private async toggleFavorite(): Promise<void> { const ids = await DataStore.loadFavorites() const idx = ids.indexOf(this.expId) if (idx >= 0) { ids.splice(idx, 1) this.isFavorite = false } else { ids.push(this.expId) this.isFavorite = true } await DataStore.saveFavorites(ids) }

这段代码先读取当前 ID 数组,再根据expId是否存在决定添加或移除。页面状态isFavorite会立即更新,最后保存到本地。它没有防重复添加,因为idx >= 0已经覆盖了重复点击场景。

6. 收藏按钮:UI 状态来自 isFavorite,而不是列表长度

实验页右上角按钮根据isFavorite切换颜色:

Row() { Text(this.isFavorite ? '★' : '☆') .fontSize(20) .fontColor(this.isFavorite ? AppColors.ACCENT_GREEN : AppColors.TEXT_SECONDARY) } .width(40) .height(40) .borderRadius(20) .backgroundColor('#111827') .justifyContent(FlexAlign.Center) .onClick(() => { this.toggleFavorite() })

源码输出中图标字符可能因为编码显示为乱码,但结构可以确认:按钮显示由isFavorite控制,点击调用toggleFavorite()

这里有一个值得保留的原则:收藏按钮不应该每次渲染都重新读取 Preferences。读取本地数据是异步操作,频繁放在 UI 构建路径里会让页面状态不可控。当前源码在生命周期中加载一次,点击时更新一次,是合理的。

7. FavoritesPage:收藏列表通过 ID 映射实验定义

收藏页只保存一个状态:

@State favorites: Experiment[] = []

加载逻辑是:

private async reload(): Promise<void> { const ids = await DataStore.loadFavorites() const all = getAllExperiments() const list: Experiment[] = [] for (const id of ids) { const found = all.find(e => e.id === id) if (found) { list.push(found) } } this.favorites = list }

这段代码把本地 ID 数组转换成当前实验定义数组。它有一个很实用的容错:如果某个 ID 在getAllExperiments()中找不到,就跳过,不让列表出现空对象。

这也解释了为什么收藏本地只保存 ID:收藏页展示的名称、描述、图标、分类、难度都来自模型最新定义,而不是历史缓存。

8. 生命周期 reload:解决返回页面后的列表刷新

收藏页和笔记页都使用了两个生命周期入口:

aboutToAppear(): void { this.reload() } onPageShow(): void { this.reload() }

aboutToAppear()负责页面首次进入时加载;onPageShow()负责页面重新显示时刷新。对于收藏列表尤其重要:用户可能从收藏页进入实验模拟页,切换收藏状态后返回收藏页。如果只在首次进入加载,列表就可能显示旧数据。

这类本地数据页面一般要遵守一个简单规则:

页面刷新时机
详情页按钮状态进入详情时读取一次,点击时更新
列表页首次进入和返回显示时重新读取
统计页依赖 AppStorage 快照或进入时重新计算

当前FavoritesPageNotesPage都选择了进入/显示时重新加载,适合本地轻量数据。

9. 收藏空态:没有数据时给用户下一步

收藏页空态代码:

if (this.favorites.length === 0) { Column() { Text('暂无收藏') .fontSize(16) .fontColor(AppColors.TEXT_HINT) Text('去实验室收藏你感兴趣的实验吧') .fontSize(13) .fontColor(AppColors.TEXT_HINT) .margin({ top: 8 }) } .width('100%') .layoutWeight(1) .justifyContent(FlexAlign.Center) .alignItems(HorizontalAlign.Center) }

空态不是装饰,它告诉用户下一步该去哪:去实验室收藏实验。对学习工具来说,这比只显示空白页面更清楚,也能避免用户误以为数据加载失败。

如果后续要增强,可以在空态加入“去实验室”按钮,直接路由到实验列表。但当前源码没有这个按钮,文章只描述现有提示文案。

10. 收藏列表卡片:点击后回到实验模拟页

收藏列表的卡片点击会进入实验模拟:

.onClick(() => { router.pushUrl({ url: 'views/experiment/ExperimentSimPage', params: { expId: exp.id, expName: exp.name } }) })

这里传入expIdexpName。实验模拟页再通过router.getParams()读取这些参数,初始化当前实验。

这条链路说明收藏列表是实验入口,不是知识点收藏入口。它没有传kpTitlekpSummary,也没有记录知识点 ID。知识详情页虽然存在getRelatedExperiment()这样的映射逻辑,但当前收藏页的持久化对象仍是实验 ID。

这就是本文收窄标题的原因:源码支持“实验收藏与本地笔记”,不支持“知识详情、收藏列表和本地记录三方同步”。

11. NotesPage:笔记是独立的本地数组

笔记页定义本地接口:

interface NoteItem { id: string title: string content: string timestamp: string category: string }

状态如下:

@State notes: NoteItem[] = [] @State isEditing: boolean = false

notes是展示列表,isEditing决定是否显示删除按钮。页面加载时:

private async reload(): Promise<void> { this.notes = await DataStore.loadNotes<NoteItem>() }

笔记没有和知识详情或实验结果自动绑定。它是用户手动输入的学习记录,包含标题、内容、日期和分类。这个边界很重要,因为“自动同步知识详情”会涉及路由参数、关联 ID、笔记来源、重复合并等逻辑,当前源码没有实现。

12. NoteEditorDialog:保存前拦截空标题和空内容

笔记弹窗通过@CustomDialog实现:

@CustomDialog export struct NoteEditorDialog { controller: CustomDialogController title: string = '' content: string = '' category: string = '基础' onSave: (title: string, content: string, category: string) => void = () => {} }

保存按钮里做了输入清理:

.onClick(() => { const t = this.title.trim() const c = this.content.trim() if (t.length === 0 || c.length === 0) { return } this.onSave(t, c, this.category) this.controller.close() })

这段逻辑防止空标题和空内容进入本地数组。它没有显示错误提示,只是静默返回。对于当前简洁工具页来说可以接受;如果要增强可用性,可以在弹窗内增加提示状态,例如@State errorText,但当前源码没有做。

13. 新建笔记:先更新页面状态,再保存本地

笔记页通过CustomDialogController接收保存回调:

private editorController: CustomDialogController = new CustomDialogController({ builder: NoteEditorDialog({ onSave: (title: string, content: string, category: string) => { this.addNote(title, content, category) } }), autoCancel: true, customStyle: true })

新增逻辑:

private async addNote(title: string, content: string, category: string): Promise<void> { const now = new Date() const pad = (n: number): string => (n < 10 ? '0' + n : '' + n) const ts = now.getFullYear() + '-' + pad(now.getMonth() + 1) + '-' + pad(now.getDate()) const item: NoteItem = { id: 'n_' + now.getTime(), title, content, timestamp: ts, category } this.notes = [item, ...this.notes] await DataStore.saveNotes<NoteItem>(this.notes) }

这里有几个细节:

  • id使用时间戳前缀,适合本地轻量记录;
  • timestamp只保存日期,不保存具体时分秒;
  • 新笔记插入数组头部,列表优先展示最近创建的记录;
  • 保存的是整个notes数组,不是 append 单条。

由于当前记录量不会很大,保存整个数组是简单有效的。记录量变大后,就要考虑分页、增量写入和索引。

14. 删除笔记:编辑态控制删除入口

删除逻辑也很直接:

private async removeNote(id: string): Promise<void> { this.notes = this.notes.filter(n => n.id !== id) await DataStore.saveNotes<NoteItem>(this.notes) }

UI 上只有进入编辑态才显示删除按钮:

Text(this.isEditing ? '完成' : '编辑') .fontSize(14) .fontColor(AppColors.PRIMARY) .onClick(() => { this.isEditing = !this.isEditing })

列表项里:

if (this.isEditing) { Text('✕') .fontSize(16) .fontColor(AppColors.ACCENT_RED) .onClick(() => { this.removeNote(note.id) }) }

这避免了普通浏览状态下误触删除。当前源码没有二次确认,也没有撤销。对于学习笔记这种用户输入内容,后续如果要提高安全性,应加确认弹窗或撤销提示。当前文章只按实际源码描述“编辑态删除并持久化”。

15. DataStore 的统计快照:收藏数会同步到 AppStorage

DataStore里还有一层统计通知:

private static notifyStatsChanged(key: string, value: string | number): void { if (STAT_KEYS.indexOf(key) < 0) return if (key === 'favorite_experiments') { try { const ids = JSON.parse(value as string) as string[] AppStorage.setOrCreate<number>(FAVORITE_COUNT_KEY, ids.length) } catch (_) { AppStorage.setOrCreate<number>(FAVORITE_COUNT_KEY, 0) } } DataStore.statsVersion++ AppStorage.setOrCreate<number>(STATS_VERSION_KEY, DataStore.statsVersion) }

这说明保存收藏后,不只是 Preferences 变化,AppStorage 中的收藏数量快照也会更新。这样“我的”页面或其他统计组件可以不重新解析 JSON,也能读到收藏数量。

需要注意:user_notes不在STAT_KEYS中,因此保存笔记不会触发统计快照。这也是源码边界之一。文章不能写成“笔记数量同步到全局统计”,因为当前代码没有这样的键。

16. 清理本地数据:收藏、记录和笔记一起清

LOCAL_DATA_KEYS包含:

const LOCAL_DATA_KEYS: string[] = [ 'favorite_experiments', 'experiment_records', 'user_notes', 'experiment_count', 'learning_seconds', 'learning_minutes' ]

clearCache()会删除这些键,并重置统计快照:

static async clearCache(): Promise<void> { if (!DataStore.prefInstance) return for (let i = 0; i < LOCAL_DATA_KEYS.length; i++) { const key = LOCAL_DATA_KEYS[i] try { await DataStore.prefInstance.delete(key) } catch (_) { } } try { await DataStore.prefInstance.flush() } catch (_) { } AppStorage.setOrCreate<number>(FAVORITE_COUNT_KEY, 0) AppStorage.setOrCreate<number>(EXPERIMENT_COUNT_KEY, 0) AppStorage.setOrCreate<number>(LEARNING_SECONDS_KEY, 0) }

这意味着收藏和笔记都属于“本地缓存/本地学习数据”的一部分。清理动作会影响用户收藏和笔记,产品文案必须明确。当前这篇文章只讨论数据层实现,不假设已有完整的清理确认流程。

17. 适合迁移的服务边界

当前源码中DataStore已经承担了数据入口职责,但收藏和笔记业务规则仍分散在页面里。如果后续功能变多,可以继续抽出服务层:

export class FavoriteService { static async toggleExperiment(expId: string): Promise<boolean> { const ids = await DataStore.loadFavorites() const index = ids.indexOf(expId) if (index >= 0) { ids.splice(index, 1) await DataStore.saveFavorites(ids) return false } ids.push(expId) await DataStore.saveFavorites(ids) return true } }

页面可以改成:

private async toggleFavorite(): Promise<void> { this.isFavorite = await FavoriteService.toggleExperiment(this.expId) }

这样页面只关心按钮状态,收藏数组的读写、去重、持久化都放到服务里。当前源码还没有这个服务层,因此这是后续可迁移写法,不是现有实现。

18. 验证清单:从实验页、收藏页、笔记页分别测

验证收藏链路:

操作预期
从实验列表进入某个实验模拟页右上角收藏按钮根据本地 ID 状态显示
点击收藏按钮favorite_experiments增加当前expId
返回我的收藏页列表出现该实验卡片
再次进入实验页取消收藏本地 ID 被移除,收藏页 reload 后不显示
模型删除某实验 ID收藏页跳过找不到的 ID,不渲染空卡

验证笔记链路:

操作预期
打开我的笔记,列表为空显示“暂无笔记”空态
点击新建笔记,标题或内容为空保存不新增记录
输入标题、内容、分类后保存新笔记插入列表顶部
退出再进入笔记页DataStore.loadNotes()重新加载本地数组
点击编辑,再点删除该笔记从列表和本地数组移除

验证数据层:

操作预期
初始化 DataStore 失败页面读取默认空数组,不崩溃
收藏 JSON 损坏loadFavorites()返回空数组
笔记 JSON 损坏loadNotes()返回空数组
清理缓存收藏、实验记录、笔记和学习时长键被删除

19. 常见问题与修复方向

问题可能原因修复方向
收藏按钮状态不对进入实验页前没有更新expId先读取路由参数,再调用loadFavoriteState()
收藏页返回后不刷新只在首次进入加载数据onPageShow()中也调用reload()
收藏列表出现空白卡本地 ID 找不到模型映射时过滤found为空的记录
删除笔记后重进又出现只改了notes状态,没有保存删除后调用DataStore.saveNotes()
空笔记被保存弹窗保存前没有trim()校验保存前拦截空标题和空内容
收藏数量统计不变保存后没有触发统计快照通过putString()调用notifyStatsChanged()
清理缓存后 UI 还显示旧数量AppStorage 快照没归零clearCache()同步重置统计键

这些问题都能从当前源码里找到对应的防线。写这类页面时,不要只看按钮能不能点,还要检查页面返回、重新进入、数据损坏和清理缓存后的状态。

20. 小结:本地学习记录要靠明确的数据归属

05-10 收藏与笔记的源码实现并不复杂,但边界很清楚。

收藏属于实验维度:ExperimentSimPage切换收藏状态,DataStore保存favorite_experimentsFavoritesPage重新加载 ID 并映射到实验模型。

笔记属于用户手动记录:NoteEditorDialog收集标题、内容和分类,NotesPage生成NoteItemDataStore保存user_notes数组。

本地数据属于 Preferences:DataStore统一封装读写、JSON 解析、默认值兜底、统计快照和缓存清理。页面不直接操作 Preferences,这让 UI 逻辑保持简单。

当前源码支持实验收藏与手动学习笔记的本地持久化;没有知识详情收藏、笔记自动同步、云同步或账号体系。把这个边界写清楚,是技术文章可复核的前提,也是后续继续扩展服务层、同步层或搜索能力时的工程起点。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询