云计算百科
云计算领域专业知识百科平台

【细胞工坊|10】HarmonyOS ArkTS 收藏与笔记实战:把实验收藏和本地笔记写进 Preferences

部分内容由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 应用里,“收藏”和“笔记”看起来只是两个入口,但实际会牵涉多个页面:实验模拟页要能切换收藏状态,我的收藏页要能重新加载列表,笔记页要能新增和删除,数据层要能把这些记录稳定保存下来。

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

问题表现源码里的处理方式
收藏按钮状态和列表不一致 实验页点了收藏,返回列表看不到 FavoritesPage 在 aboutToAppear/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 快照或进入时重新计算

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

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 }
})
})

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

这条链路说明收藏列表是实验入口,不是知识点收藏入口。它没有传 kpTitle、kpSummary,也没有记录知识点 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_experiments,FavoritesPage 重新加载 ID 并映射到实验模型。

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

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

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

赞(0)
未经允许不得转载:网硕互联帮助中心 » 【细胞工坊|10】HarmonyOS ArkTS 收藏与笔记实战:把实验收藏和本地笔记写进 Preferences
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!