拓冰建站拓冰建站
首页 / 资讯中心 / 正文

HarmonyOS个人日记本应用源码解析:RDB与ArkUI实战

简介这份源代码是一套基于 HarmonyOS 开发的个人日记本应用完整实现适合正在学习鸿蒙应用开发的初学者或需要快速搭建记录类产品的开发者。应用支持文字、图片、音频、视频等多媒体日记记录并提供数据加密、智能提醒与多设备同步能力功能上比普通文本工具更完整。资源包共101个文件压缩后约391KB以26个ets页面文件、39个png UI资源为主辅以11个json、9个json5配置文件及7个ts脚本覆盖工程配置、页面逻辑与静态素材。从目录结构来看包含登录页、主页、日记列表、写日记等页面模块以及数据库操作、下拉刷新等工具封装清晰展示了HarmonyOS应用从界面搭建、数据持久化到交互优化的完整链路便于二次开发和学习排错。目前已有437人学习下载结合完整源码与清晰模块划分是上手HarmonyOS个人应用开发的实用参考资料。1. 个人日记本应用凭什么值得用 HarmonyOS 原生源码重写一版看到“基于 HarmonyOS 开发的一款个人日记本应用 APP 源代码”这个大标题先纠正一个很容易出现的判断日记本看似是把 SQLite 换成 RDB、把 RecyclerView 换成 List 就能照搬过来的低难度项目。真把代码铺开之后麻烦全在看不见的地方编辑页每敲一个字就去写一次数据库列表项内容改了却不刷新老用户升级版本后发现新字段没有加上备份文件从手机导出后表情符号变成乱码。HarmonyOS 的 ArkTS 对状态管理做得比传统命令式 UI 更直接同时系统自带的关系型数据库能覆盖绝大多数本地日记场景恰好适合做这种“数据不多、内容敏感、要长期维护”的工具型 APP。这篇文章按本项目常见的工程做法从源码工程结构、RDB 数据层、ArkUI 页面到真机签名把完整链路讲一遍。2. 源码工程结构与 RDB 数据层建表、查询和事务一次定好2.1 先看个人日记本 APP 的工程切分我收到一份 HarmonyOS 日记本源码后第一件事不是看页面效果而是看entry/src/main/ets下面有没有把 store、model、pages 拆开。个人日记本虽然业务不大但如果把 SQL 语句直接写在IndexPage.ets的按钮里后面加一个搜索功能都会变得很痛苦。一个常见的源码结构如下entry/src/main/ets/ ├── entryability/EntryAbility.ets ├── pages/ │ ├── IndexPage.ets │ └── EditPage.ets ├── model/DiaryNote.ets ├── store/NoteStore.ets └── common/DateUtils.etspages只负责渲染和事件回调store/NoteStore.ets负责所有数据库操作model/DiaryNote.ets放实体定义。这个拆法的收益在真机 Debug 时最明显页面报错不会拉着 SQL 一起报SQL 报错也不会牵出 UI 状态问题。2.2 用 relationalStore 建 diary 表HarmonyOS 的 RDB 接口对应ohos.data.relationalStore在 API 12 工程里通常写成import { relationalStore } from kit.ArkData。建表最小代码是// entry/src/main/ets/store/NoteStore.ets import { relationalStore } from kit.ArkData; import { common } from kit.AbilityKit; const DIARY_TABLE diary_table; export class NoteStore { private store?: relationalStore.RdbStore; async init(context: common.Context): Promisevoid { const config: relationalStore.StoreConfig { name: diary.db, securityLevel: relationalStore.SecurityLevel.S1 }; this.store await relationalStore.getRdbStore(context, config); await this.store.executeSql( CREATE TABLE IF NOT EXISTS ${DIARY_TABLE} ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, content TEXT, mood TEXT, weather TEXT, created_time INTEGER NOT NULL, updated_time INTEGER NOT NULL ) ); } }这段代码里securityLevel决定了数据库的安全等级S1 表示一般数据S2 到 S3 会引入更严格的访问控制。个人日记内容不属于公开信息我一般会直接把它调到SecurityLevel.S3代价是极端情况下数据库加解密的耗时略有增加。建表时把id设为主键并自增created_time和updated_time存 Unix 毫秒时间戳而不是字符串排序、按天分组、做备份去重都会省事很多。下面这张字段表作为源码里的注释放在NoteStore.ets文件头部比任何文档都有效列名类型用途源码注意点idINTEGER主键自增导入备份时不要硬塞旧 idtitleTEXT日记标题允许为空但列表页要有兜底文案contentTEXT正文不建索引避免脏数据进入全文检索moodTEXT心情标签建议存固定枚举值如happy/sadweatherTEXT天气标签旧版本升级时可能不存在created_timeINTEGER创建时间毫秒时间戳updated_timeINTEGER修改时间列表排序和 diff key 都依赖它2.3 插入和分页查询事务与 ResultSet 别踩坑日记应用最常见的初始化场景是把本地 JSON 备份导入数据库或者从旧表批量迁移。逐条insert性能很差正确做法是包一层事务async insertMany(notes: DiaryNote[]): Promisevoid { if (!this.store) { return; } this.store.beginTransaction(); try { for (const note of notes) { const values: relationalStore.ValuesBucket { title: note.title, content: note.content, mood: note.mood, weather: note.weather, created_time: note.createdTime, updated_time: note.updatedTime }; await this.store.insert(DIARY_TABLE, values); } this.store.commit(); } catch (e) { this.store.rollBack(); throw e; } }beginTransaction()和commit()必须成对出现rollBack()只放在 catch 里。这里有一个容易踩的坑如果insert循环体里嵌套了其他事务或者事务内有 await 且中间抛错却忘记 rollBack后续所有写入都会卡死。我在真机上调试某次迁移时就是因为事务没回滚导致日记列表只能读不能写。列表页分页查询是另一个高频代码段async queryPage(page: number, pageSize: number): PromiseDiaryNote[] { const predicates new relationalStore.RdbPredicates(DIARY_TABLE); predicates.orderByDesc(updated_time) .limitAs(pageSize, page * pageSize); const rs await this.store!.query(predicates); const result: DiaryNote[] []; while (rs.goToNextRow()) { result.push({ id: rs.getLong(rs.getColumnIndex(id)), title: rs.getString(rs.getColumnIndex(title)), content: rs.getString(rs.getColumnIndex(content)), mood: rs.getString(rs.getColumnIndex(mood)), weather: rs.getString(rs.getColumnIndex(weather)), createdTime: rs.getLong(rs.getColumnIndex(created_time)), updatedTime: rs.getLong(rs.getColumnIndex(updated_time)) }); } rs.close(); return result; }limitAs的第一个参数是本次取多少条第二个参数是偏移量。源码里如果不小心把page和pageSize传反表现形式是列表一直不出数据而不是崩溃排查时容易被忽略。rs.close()一定要执行否则 ResultSet 没释放下一次打开页面时旧游标还在占用连接。对于规模不大的日记表每页 20 条足够不要因为写了queryPage就真的敢一次性SELECT *。3. ArkUI 页面源码时间线列表、Observed 模型与防抖保存3.1 列表页的 ForEach key 必须带 updated_timeArkUI 的列表页源码里最常见的不是List写错而是ForEach的 key 生成器导致列表项不更新。个人日记本这种内容频繁修改的 APP千万别只把note.id当作 key。Entry Component struct IndexPage { State notes: DiaryNote[] []; private store new NoteStore(); private page 0; private pageSize 20; build() { List({ space: 8 }) { ForEach( this.notes, (note: DiaryNote) { ListItem() { this.noteItem(note) } }, (note: DiaryNote) ${note.id}_${note.updatedTime} ) } .width(100%) .height(100%) .onReachEnd(() this.loadMore()) } Builder noteItem(note: DiaryNote) { Column() { Row() { Text(note.title) .fontSize(18) .layoutWeight(1) Text(DateUtils.format(note.updatedTime)) .fontSize(12) .fontColor(#888888) } .width(100%) Text(note.content) .maxLines(2) .textOverflow({ overflow: TextOverflow.Ellipsis }) .fontSize(14) .margin({ top: 4 }) } .padding(12) .backgroundColor(#FFFFFF) .borderRadius(12) .margin({ bottom: 8 }) } }key 生成器里拼上updatedTime后用户修改日记内容并点击保存数据库里的updated_time变化列表项才会被 ArkUI 识别为“这个位置的数据变了”。如果只写note.idArkUI 会认为该 key 没有变化正文已经改了但界面纹丝不动这是个人日记本源码里最典型的 UI 缓存问题。3.2 让实体类可观察Observed 与 ObjectLink列表项数据在子组件里发生局部修改时只有类被标记为可观察UI 才会及时刷新。我在源码里通常让DiaryNote继承一个被Observed修饰的基类或者在子组件里使用ObjectLink接收对象Observed export class DiaryNote { title: string ; content: string ; mood: string ; weather: string ; updatedTime: number 0; constructor(init?: PartialDiaryNote) { if (init) { Object.assign(this, init); } } }在子组件卡片中声明ObjectLink note: DiaryNote父组件里仍然用State notes: DiaryNote[]持有数组。两者配合后修改note.title这类深层字段会触发当前组件更新不用手动重建整个数组。顺带说一句State只观察数组项的重置和新增删除并不会自动观察普通 class 内部字段变化这个边界用Observed补齐即可。3.3 编辑器自动保存要用防抖日记编辑器是一个典型的高频输入场景。如果TextArea的onChange回调里直接写 RDB用户快速输入时键盘每敲一个字就触发一次事务很快就能在 hilog 里看到大量数据库写操作堆积。处理方式是加 500 毫秒防抖private timerId: number -1; onContentChange(value: string) { this.draft.content value; if (this.timerId 0) { clearTimeout(this.timerId); } this.timerId setTimeout(() { this.saveDraft(); this.timerId -1; }, 500); } aboutToDisappear(): void { if (this.timerId 0) { clearTimeout(this.timerId); this.saveDraft(); } }代码逻辑不算复杂但要注意aboutToDisappear里的兜底用户输入一段话后马上返回列表如果只靠防抖的 500 毫秒页面销毁时定时器被系统终止最后一次修改就没有落库。clearTimeout后立刻调用一次saveDraft是个人日记本 APP 必须做的收尾动作。编辑器源码还有一个可以顺手优化的地方saveDraft内部只 update 改过的字段不要每次把title、content、weather全量塞进ValuesBucket减少无谓的旧值覆盖。4. 日记数据的备份恢复与表结构迁移源代码4.1 用 PRAGMA user_version 做 RDB 升级HarmonyOS 的 RDB 不是每次都会自动帮你改表CREATE TABLE IF NOT EXISTS只能保证建表不能给老表补新列。个人日记本经常会从第一版只存title/content升级到存weather/mood这时候需要自己维护一个版本号。常见做法是使用 SQLite 自带的PRAGMA user_versionasync migrate(): Promisevoid { const rs await this.store!.querySql(PRAGMA user_version); let version 0; if (rs.goToNextRow()) { version rs.getLong(0); } rs.close(); if (version 2) { await this.store!.executeSql( ALTER TABLE diary_table ADD COLUMN weather TEXT ); await this.store!.executeSql(PRAGMA user_version 2); } }这段代码要在init()建表之后执行。注意PRAGMA user_version只能表示整数版本号所以每次升级都写死一个递增的数字。不要把ALTER TABLE直接放在应用启动的主流程之外因为如果用户设备上既有版本 1 又有版本 0 的表条件必须用 2而不是 1否则跳过升级的用户永远等不到新字段。4.2 导出备份 JSON 并在源码层做字段校验备份功能是日记应用区别于普通备忘录的核心。我一般不直接把.db文件原样复制给用户因为数据库文件里可能包含索引碎片和未回收空间导出为 JSON 后在导入端更可控。export function buildBackupJson(notes: DiaryNote[]): string { const payload { app: harmony-diary, schemaVersion: 2, exportedAt: Date.now(), notes: notes.map(n ({ title: n.title, content: n.content, mood: n.mood, weather: n.weather, createdTime: n.createdTime, updatedTime: n.updatedTime })) }; return JSON.stringify(payload); }导出文件不要保存id字段。备份用于换机或重装场景旧id在新库里没有任何意义带着反而会让AUTOINCREMENT自增序列错乱。schemaVersion是必须写的以后备份格式增加字段时可以用它判断是否要执行兼容转换。真机上保存备份文件时把 JSON 先写入应用沙箱再通过picker.DocumentViewPicker让用户选择保存位置。不要直接在源码里写死一个公共目录路径一来是应用权限不允许二来是用户在“文件管理”里也找不到体验非常差。4.3 导入恢复时的合并策略导入备份最容易弄脏数据。源码里我会先解析 JSON再按createdTime title做一次去重最后把不重复的日记批量写入事务async importNotes(backupNotes: DiaryNote[]): Promisenumber { let inserted 0; this.store!.beginTransaction(); try { for (const note of backupNotes) { const predicates new relationalStore.RdbPredicates(DIARY_TABLE); predicates.equalTo(created_time, note.createdTime) .and() .equalTo(title, note.title); const rs await this.store!.query(predicates); const exists rs.goToNextRow(); rs.close(); if (!exists) { await this.insertOne(note); inserted; } } this.store!.commit(); return inserted; } catch (e) { this.store!.rollBack(); throw e; } }重复判断使用createdTime加title比单独判断title安全因为同一天里“周末”出现两次也是合理的。如果旧备份缺少createdTime导入时可以在解析层补一个当前时间戳但这样会导致每导一次都生成一条新日记所以备份导出字段里必须带上时间戳。5. 把个人日记本 APP 签名装进真机构建命令与验证技巧源码写完后最重要的一步是把 HAP 装上真机。DevEco Studio 里直接点运行最省事但用命令行更可控也容易在 CI 里复用hvigorw clean hvigorw assembleHap --mode module -p productdefault hvigorw signHap hdc install -r entry/build/default/outputs/default/entry-default-signed.hap这段命令中hvigorw clean负责清掉上一轮编译产物避免增量编译把旧的资源文件带进去signHap会读取项目中配置的签名文件提示hdc install -r的-r参数表示覆盖安装也就是保留应用数据重新安装日常调试时不需要每次都卸载旧包。签名这一步是个人日记本源码最容易卡住的地方。调试签名和发布签名不同真机调试只需要在 DevEco Studio 里把“Automatically generate signature”打开让 IDE 自动生成证书和 profile 即可可以不用先申请正式发布证书。如果命令行签名报错先检查build-profile.json5里signingConfigs指向的.cer、.p7b文件是否存在再用 DevEco Studio 手动重新生成一次。以下是真机调试时我常用的验证清单现象验证方法问题源头安装报签名错误hdc shell bm dump -n com.example.diary查看 bundleName签名 profile 和工程包名不一致列表改文字后不刷新hdc shell hiloggrep ArkUI 查看 ForEach key自动保存掉数据日志中观察saveDraft时间点缺少aboutToDisappear兜底数据库升级后崩溃hdc shell hiloggrep Rdb备份文件保存失败查看应用沙箱是否有写入权限未先写沙箱再用 DocumentViewPicker 转存最后补一个验证启动页的命令hdc shell aa start -b com.example.diary -a EntryAbility。如果应用在桌面点开正常但命令启动失败多半是module.json5里EntryAbility的exported属性为false调整后再装一次。个人日记本 APP 的源码要从“能编译”到“真机可发布”这几个点比多写十个页面都要重要。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门