做这个HarmonyOS日记应用,最开始真的只是想要一个能随手记点东西的本地工具,后来越做越深,逐渐加上了语音、情绪分析、跨设备同步,整个过程可以拆成三个版本来看。如果你也在HarmonyOS上做过应用功能迭代,从“能记”到“懂你”的这个过程,这篇实践笔记应该能帮你少踩不少坑。
1. 项目演进:从“能记”到“好记”再到“懂你”
1.1 第一版:只解决“能写进去”的问题
第一版的需求非常朴素:打开应用,写一段文字,保存,下次还能看到。我当时用的是最直接的技术组合,页面用ArkUI写列表和编辑框,数据存在首选项(Preferences)里,整个应用只有一个页面栈,日记列表按时间倒序排列,点击某一条进详情,再点编辑能改内容。
这个版本跑通以后,问题立刻暴露出来了。首选项本身是给轻量键值数据用的,存结构化日记列表很别扭,每条日记还要拼接ID、标题、正文、时间戳,读取时要手工解析。更难受的是,日记一多,列表滚动性能明显下降,因为所有数据都是一次性加载到内存里的,没有分页概念。我意识到,应用如果要往前走,数据层必须换成正经的数据库。
另一个痛点是“找不回内容”。日记这个东西,用户最在意的其实是回顾。当时只能一条一条往下翻,没有搜索、没有标签、没有日期维度。有个朋友给我反馈,说一个月前写过某次体检的注意事项,现在找不到,只能硬翻,很崩溃。
1.2 第二版:结构化与多媒体
第二版的核心目标是把数据管好。我把存储从首选项迁到了关系型数据库(RDB),建了日记主表,字段包括id、content、mood、tags、createTime、updateTime、weather、location,还单独建了附件表,用外键关联日记ID,图片、录音文件的路径都存附件表。查询的时候按时间分页,配合LIKE模糊搜索,标题和正文都能搜。
也就是从这一版开始,日记不再只是纯文字。我给编辑器加了拍照和录音入口,图片会压缩后存到应用沙箱目录,数据库里只存相对路径。录音用系统提供的音频录制能力完成,录音时长限制在五分钟以内,因为实测下来,超过五分钟的语音日记回放率很低,用户根本没有耐心重新听。
结构化之后,产品能力上一个台阶。日期维度的日历视图加进来,每天有没有日记、写了多少字,一眼就能看清。这其实是在为后续的“习惯养成”场景做铺垫。我真实感受到,用户开始把日记当成一个每天都会打开的应用,而不只是偶尔的记录工具。
1.3 第三版:智能与分布式
第二版上线跑了一段时间,数据沉淀得越来越多,新的用户诉求开始出现。有人问:能不能把以前的日记自动汇总成上个月的月度回顾?有人问:我在手机上写了半截,想在平板上继续写,能不能同步?还有人问:不想打字,能不能直接说话生成日记?
这些需求指向两个方向:一个是AI能力的注入,另一个是跨设备数据流转。这两个方向的实现难度都不小。第三版我主要做了四件事:语音转文字的日记录入、情绪维度的自动识别、老日记的摘要生成、以及基于系统分布式数据能力的多设备同步。
为什么选择用系统自带的AI服务而不是自己训练模型?原因很现实:日记类应用核心价值是记录和回顾,不是自然语言处理。接入现成的、端侧能力优先的AI接口,开发成本低、隐私风险小,体验也稳定。我自己在项目里做过对比,如果全部自研,至少要多花两个月的工程量,效果还不一定比现成方案好。
| 版本 | 核心能力 | 存储方案 | 用户价值 |
|---|---|---|---|
| V1 | 本地文字记录 | 首选项 | 能随时随地写 |
| V2 | 搜索、标签、日历、多媒体 | 关系型数据库 | 内容可管理、可回顾 |
| V3 | 语音转写、情绪分析、摘要、跨端同步 | 关系型数据库 + 分布式数据服务 | 记录更高效、回顾智能、多端无感切换 |
2. 核心技术拆解与关键选型
2.1 工程模型:Stage 模型为什么比 FA 模型更适合
HarmonyOS应用早期有FA(Feature Ability)模型和Stage模型两套体系。新项目建议直接用Stage模型,这个结论是我在实际迁移中被教育出来的。FA模型的Ability更像是“以页面为中心”的组件,逻辑分散,模块间的数据传递依赖Intent和全局变量,在应用复杂化以后代码会变得很拧巴。
Stage模型的核心变化是“Ability + WindowStage + UIAbility”的分层方式,应用级别的生命周期和应用内页面栈分离,组件复用和模块化拆分会清晰很多。我在重构时把工程拆成了entry主模块和feature日记组件、feature统计组件两个共享模块,通过路由表解耦页面跳转,编译期还能做按需打包。对于一个要持续迭代的日记应用来说,这种结构的维护成本远低于堆在单个模块里的方式。
Stage模型下,EntryAbility负责启动入口,Login页面、主页面都通过路由表跳转。实际开发中最直观的感受是:不同模块之间的数据访问不再到处传Intent参数,而是通过统一的Service层管理,代码清晰度提升明显。
2.2 数据层选型:三种存储方式到底怎么搭配
HarmonyOS给开发者提供了好几种持久化方案,我在项目里用到了其中三类,核心选择逻辑可以做成一张对比表:
| 方案 | 适用场景 | 特点 |
|---|---|---|
| Preferences(首选项) | 设置项、用户偏好 | 轻量级、键值存储、读写简单 |
| RDB(关系型数据库) | 结构化业务数据 | SQL操作、索引、事务、适合复杂查询 |
| 分布式数据服务 | 多设备同步数据 | 基于设备组网自动同步,有冲突处理机制 |
日记应用的核心数据表选RDB,最核心的原因是查询需求复杂:要按日期范围查、按标签筛选、按关键词模糊搜、按情绪维度聚合统计。首选项做这些操作非常吃力,而RDB可以用SQL直接解决。
当然,RDB也不是没有麻烦。第一是建表迁移,HarmonyOS的RDB接口在版本升级后如果表结构变化,需要自己处理旧版本数据库的迁移逻辑,我在某个内测版本里因为改了表结构没有写迁移脚本,导致一批用户升级后白屏,这个坑后面会详细说。
除了核心数据,我把用户设置(比如提醒时间、页面偏好、字体大小)放在Preferences里,数据结构简单,读写频率低,不需要数据库的重量级事务。
分布式数据服务用于跨设备同步。需要注意的一点是:它并不直接替代RDB,而是把需要同步的数据子集复制到分布式数据库里,客户端通过订阅变化来更新本地RDB。也就是说,RDB是本地事实源,分布式库是同步通道,两者配合而不是互相替代。
// 数据模型定义部分,ArkTS实现 export class DiaryEntry { id: number = 0; content: string = ''; mood: string = ''; tags: string[] = []; weather: string = ''; createTime: number = Date.now(); updateTime: number = Date.now(); deleted: boolean = false; }// RDB建表与基础CRUD片段 import { relationalStore } from '@kit.ArkData'; const CREATE_TABLE_SQL = ` CREATE TABLE IF NOT EXISTS diary ( id INTEGER PRIMARY KEY AUTOINCREMENT, content TEXT NOT NULL, mood TEXT DEFAULT '', tags TEXT DEFAULT '[]', createTime INTEGER NOT NULL, updateTime INTEGER NOT NULL, deleted INTEGER DEFAULT 0 ) `; async function initDb(store: relationalStore.RdbStore) { await store.executeSql(CREATE_TABLE_SQL); } async function insertDiary(store: relationalStore.RdbStore, entry: DiaryEntry) { const values = new relationalStore.ValuesBucket(); values.put('content', entry.content); values.put('mood', entry.mood); values.put('tags', JSON.stringify(entry.tags)); values.put('createTime', entry.createTime); values.put('updateTime', entry.updateTime); await store.insert('diary', values); } async function queryDiaryByPage(store: relationalStore.RdbStore, page: number, pageSize: number) { const predicates = new relationalStore.RdbPredicates('diary'); predicates.equalTo('deleted', 0); predicates.orderByDesc('createTime'); predicates.limitAs((page - 1) * pageSize, pageSize); return await store.query(predicates); }你在自己项目里做存储选型时,我的建议是不要只看ORM的文档,先画一遍核心场景的查询条件。如果查询条件里有超过两个维度的组合筛选,直接上RDB;如果只是缓存网络返回结果,Preferences甚至文件就够了。存储选型这种事,选轻了后面要重写,选重了一开始代码复杂度就高,多花时间在理解业务上比什么都值。
2.3 UI层:ArkUI声明式状态管理带来的改造红利
界面这一层,我用的是ArkUI声明式开发。初版不太适应,尤其是状态更新机制,习惯了Android的命令式setText之后,刚开始用@State和@Link管理界面刷新总觉得“不受控制”。但用了几天之后反而回不去了,因为声明式的写法让UI状态和数据状态在语义上对齐了:只要数据变了,界面自动更新,不需要手动找控件ID去刷新。
在日记编辑器里,有一个典型场景:用户切换“心情”标签时,整个页面的背景色、天气选择区、推荐标签区都要联动变化。如果用手动刷新,要写一堆setState逻辑;用ArkUI的话,只需要把当前mood作为@State变量,页面里跟mood相关的绑定属性都写成表达式,联动是自动完成的。
列表页的性能是另一个重点。当日记条数到几百上千条时,普通ForEach渲染会有明显卡顿。我改成LazyForEach懒加载后,滚动流畅度提升不少。特别是列表项的图片缩略图,必须做二级缓存策略:首次加载生成缩略图,后续直接读缓存文件,内存和磁盘双缓存。
Navigation路由这块也值得说。原来用页面栈的方式管理跳转,日记编辑页返回列表页后,列表数据要通过回调或者刷新事件重新读。后来改用Navigation的NavPathStack,配合生命周期监听,编辑页返回时自动触发列表页的onNavBackPage回调,数据刷新就不用自己做通知了。
3. 智能功能落地实操
3.1 语音转写:权限申请是第一个坑
语音录入是第三版里用户感知最强的功能。说真的,一个日记应用支持“说话即记录”,体验提升是质的飞跃。技术实现上,核心是调用系统语音识别能力,把音频流转成文字,然后填入编辑器。
第一步是权限动态申请。HarmonyOS的麦克风权限属于敏感权限,必须在真机上动态请求,模拟器没有麦克风硬件,调试时会直接走失败分支。我一开始在模拟器上测试,还以为是代码问题,换真机一测就好了。
// 动态权限申请片段 import { abilityAccessCtrl, Permissions } from '@kit.AbilityKit'; async function requestMicrophonePermission(context: common.UIAbilityContext): Promise<boolean> { const atManager = abilityAccessCtrl.createAtManager(); const permissions: Permissions[] = ['ohos.permission.MICROPHONE']; const res = await atManager.requestPermissionsFromUser(context, permissions); return res.authResults[0] === 0; }权限拿到之后,语音识别的调用相对规范。先创建语音识别器配置,适配采样率和音频格式,然后传入录音音频流,识别结果通过回调返回。需要注意识别过程是异步的,而且对网络环境有依赖,在线识别在信号弱的环境下延迟明显,所以我在实现时加了超时重试机制,连续识别失败超过三次就降级为文字输入,不让用户卡在录音等待里。
实际测试中还有一个细节:录音文件的格式要统一。我踩过格式混用的坑,同一段语音在真机上录出来是m4a,代码里写死的解码逻辑只处理wav,结果回放直接黑屏。后来统一封装了一个录音工具类,所有录音统一转成m4a编码格式,再交给识别模块。
3.2 情绪识别与标签提取:离线规则兜底
情绪识别这件事,本质上要做的是“文本 -> 情绪标签”的映射。我刚上线时直接调系统自然语言处理能力,服务稳定但有一个问题:短文本的识别结果不太稳定。“今天好累”四个字,有时候识别成“疲劳”,有时候又识别成“低落”,同样是负面情绪,标签不一致对统计是灾难。
后来我调成了“本地规则 + 在线模型”双路方案。本地维护一个常用的情绪词规则库,比如“累”“疲惫”“加班”映射到“疲劳”,“开心”“顺利”“成功”映射到“喜悦”,“焦虑”“担心”“压力”映射到“焦虑”。短文本优先走规则库,规则库匹配不到再走在线模型;长文本则直接走在线模型,因为规则库对复杂语义无能为力。
实测下来,这个双路方案把情绪标签的一致率提升到了接近九成。涉及具体技术时:
// 情绪解析封装片段 function analyzeMood(content: string): string { if (content.length <= 50) { const mood = matchRules(content); if (mood) { return mood; } } // 在线模型兜底,伪代码示意 return callMoodModel(content); } function matchRules(content: string): string { const rules: Record<string, string[]> = { 'joy': ['开心', '顺利', '成功', '好棒', '值得'], 'fatigue': ['累', '疲惫', '加班', '熬夜'], 'anxiety': ['焦虑', '担心', '压力', '害怕'], }; for (const mood in rules) { if (rules[mood].some((word) => content.includes(word))) { return mood; } } return ''; }标签提取同样是这个思路。我会从日记文本里抽取实体词和主题词,天气、地点、人物、事项,再结合手动标签合并成最终的标签集合。抽出来的词会缓存到本地,下次同样内容不重复请求模型接口,能省不少处理开销。
3.3 智能摘要:长内容切段与缓存策略
摘要是“月度回顾”功能的基础。实现逻辑不复杂:把指定时间段的日记按天取出来,每篇超过一定长度的正文截断,然后调用模型接口要求生成本周、本月的总结要点。
一开始我以为摘要是最简单的功能,实际上踩了两个坑。
第一个坑是文本长度限制。模型接口对输入文本长度有硬限制,我刚开始把所有日记全文一股脑倒进请求体里,结果在长文本场景直接报错被拒。后来做了切片拼接:先把每天日记处理成一段浓缩信息,比如情绪主基调、最重要的一个事件、当天最高频的关键词;再把天级浓缩结果按月聚合,最后才作为输入喂给模型。这样既控制了输入规模,又不丢失关键信息。
// 段落聚合伪代码示意 function buildMonthlySummary(dailySummaries: string[]): string { const body = dailySummaries.map((s) => `- ${s}`).join('\n'); return callSummaryApi({ target: 'monthly_review', data: body.slice(0, 3000), }); }第二个坑是请求频率。月度回顾这种功能,如果用户每天打开应用都触发一次,接口费用和耗时都扛不住。我做了一个三级缓存:本地缓存最近一次生成的月度回顾,若数据日期没变直接读缓存;如果跨天或者新增日记,则判断距离上次请求超过24小时才触发重新生成;生成结果落库,下次优先走缓存。
这样设计以后,一个月最多请求30次,实际场景里每次用户打开应用看到的都是千变万化的智能内容,体验上完全够用。
3.4 数据安全与用户授权:日记有天然的私密属性
我做智能功能时,被身边朋友问得最多的问题就是:“你把我的日记发给云端处理,被泄露了怎么办?”这个信任问题直接影响到用户愿不愿意打开智能开关。
所以我在设计时定了几个原则:日记核心本体永远存储在本地,云服务只接收“经过去身份化的处理片段”;AI接口调用前必须弹出明确授权说明,说明内容包括数据用途、处理方式、保存期限;应用退出时清理接口调用日志,不保留临时文件。如果设备支持硬件级密钥库,优先使用系统密钥库对本地数据库加密,密钥不落盘。
给用户的授权界面,是第二个容易被忽略的点。我见过很多应用把权限协议塞进用户条款,用户根本不会看。我的做法是:在“智能日记”功能开启时,弹出一个HTML协议的半屏弹窗,用两行字说清楚“你的日记只会用来生成摘要和情绪统计,不会用于其他目的;数据在本地加密存储”,并提供“仅离线模式”选项。这个细节对应用评价提升帮助很大。
4. 跨设备体验与数据同步实战
4.1 为什么日记应用也要跨设备
说实话,日记应用在一开始我并没有打算做多端同步,直到自己真实用了一段时间才发现需求有多强。白天在手机上通勤时随手记几条灵感,晚上回宿舍用平板写长文,周末想在大屏设备上翻看一周的回顾,这几个场景都需要数据在不同设备间流转。
HarmonyOS在跨设备数据同步上提供了现成的基础设施。通过分布式数据服务,可以让同一账号下的设备组成一个虚拟数据集,数据自动在设备间同步。我做的是“本地RDB为主 + 分布式库同步为辅”的架构:本地数据库永远是第一数据源,所有CRUD操作都先落在本地RDB,然后通过分布式数据库的写库操作把增量同步到远端设备;远端设备收到同步变更后,再完成远端RDB的更新。
4.2 同步链路搭建与版本向量防冲突
同步搭建的过程比想象中复杂一点,核心要解决两件事:数据冲突时以哪边为准,以及设备离线又上线后如何对齐。
我采用的是带时间戳的版本向量方案。每条日记除了ID,还维护一个version字段,同步的时候同时携带更新时间和版本号。冲突检测逻辑是:
// 同步冲突解决策略示意 function resolveConflict(local: DiaryEntry, remote: DiaryEntry): DiaryEntry { if (remote.updateTime > local.updateTime) { return { ...remote, version: local.version + 1 }; } if (local.updateTime > remote.updateTime) { return { ...local, version: remote.version + 1 }; } return local; }开局时,所有设备从云侧拉取完整数据快照;运行期间,同步服务通过订阅数据变化事件增量推送。我在测试中发现一个细节:分布式数据服务推送是有延迟的,尤其大文本内容,多台设备同时在线时同步延迟可能到秒级,如果用户双端同时编辑同一条日记,大概率会出现覆盖丢失。
规避策略很简单:日记详情页如果检测到内容已被其他设备修改,直接在页面上方提示“内容在其他设备上已更新”,并提供手动刷新按钮。不做自动覆盖,信息安全大于一切。
4.3 同步现场与细节优化
跨设备同步还有一个体验层面的实际问题:在没有网络的环境下回看旧日记,不应该看到内容消失。我的是先本地RDB + 同步状态标记:每篇日记在本地表中加一个syncStatus字段,标记“未同步”“已同步”“冲突待解决”三种状态。离线状态下,所有日记照常可读,只是新增内容标记为未同步。待网络恢复时后台线程自动补推。
优化同步性能时,我给大数据字段专门做了一张附件表,图片、语音不进入分布式数据库,只同步附件描述信息。本地设备按需从原设备或云侧拉取附件内容。这样同步流量和延时都大幅降低,尤其是多篇图片日记连续同步的场景下,体验差距非常明显。
5. 常见问题排查与性能优化实录
5.1 高频问题一览
做这套应用的过程中,我在不同阶段遇到过高频问题,整理成了一张排查表,很适合直接对照处理:
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| 升级后白屏 | RDB表结构变更未做迁移 | 版本号控制,新增字段用ALTER TABLE补充默认值 |
| 语音识别一直转圈 | 麦克风权限未通过或网络弱 | 动态权限申请,增加超时和重试机制 |
| 智能摘要返回为空 | 输入文本被截断或模型超时 | 做输入分片,超时后降级为本地规则摘要 |
| 双端数据不一致 | 同一天多次编辑冲突 | 版本向量 + 同步状态提示,不自动覆盖 |
| 列表滑动掉帧 | 全部图片加载原图 | LazyForEach懒加载 + 缩略图磁盘缓存 |
| 后台同步失效 | 应用进程被系统挂起 | 使用系统可靠任务能力发起定时同步 |
其中最让我头疼的是数据库迁移的坑。某次版本改动我加了location字段,建表SQL只对新建库生效,老库升级时没有执行ALTER TABLE,结果老用户打开应用直接白屏。后来我整理了一套标准的数据库版本迁移脚本:读取当前库版本号,从低版本逐级执行迁移SQL,每个迁移步骤都放在事务里。这个教训后来救了很多次。
5.2 列表渲染和图片加载的性能优化
日记列表的一个特点是每行都有图片缩略图。在优化之前,我直接加载原图路径,导致列表滑动时GC频繁、页面卡顿。后来做了三级处理:生成缩略图(最大边长300px)存入缓存目录;列表项渲染时优先读缩略图缓存;缓存不存在时异步加载原图再生成缩略图。实测后滑动帧率提升明显,内存占用下降三成。
LazyForEach的Keys生成也很关键。如果Key使用日记的ID,那么新增数据时前面的Item会被重新创建,导致不必要的渲染损耗。我把key函数改成基于索引和ID组合生成,既保证稳定性又能精准复用已有组件。
搜索场景的性能优化,主要体现在关键词高亮上。如果每次输入都重新查询全部数据、再对全部内容做高亮处理,性能非常差。我实现的方案是:先用RDB的LIKE查询出匹配的ID集合,再对当前页实际展示的少量内容做高亮替换和分页展示。这样每个查询的IO成本和渲染成本都控制在可接受范围。
5.3 真机联调、多设备测试与发布适配
模拟器和真机的差距在日记应用上体现得非常明显。模拟器能运行基础界面,但麦克风、相机、真实网络请求、跨设备同步都无法完整模拟。所以我在功能合入主干前,都会走一遍真机联调流程:
- 双设备组网,验证同步时效和冲突场景;
- 使用不同分辨率和折叠屏形态,验证日历视图和编辑器排版适配;
- 电池和功耗场景,确认后台同步线程不会过度耗电;
- 长期运行场景,连续跑一周,观察RDB文件增长和内存泄漏。
发布前还要检查权限声明描述是否和实际调用一致。HarmonyOS的隐私审核比较严格,如果申请了麦克风权限但应用内没有语音功能,或者权限说明写得不清楚,审核阶段会有被拒风险。我的做法是每个敏感权限都在配置文件中标注明细分场景,比如“麦克风权限用于语音日记识别,仅在用户点击语音输入时调用,后台不采集音频”。
另外一个容易被忽视的适配点是深色模式。日记应用的使用场景很多在晚上,如果只做了浅色适配,用户在深色模式下界面亮度刺眼,体验会很差。我用ArkUI的色板变量和资源限定词分别配置了浅色/深色两套UI资源,切换逻辑交给系统自动完成。
6. 实测心得与后续扩展思路
6.1 给后来者的几个实在建议
如果你也想做类似的应用,有几点值得在动手前想清楚。
第一,日记应用的核心价值不是功能多,而是稳定性。用户把私密内容交给你,如果某天打开发现数据丢了、同步乱了,以后就很难再信任你。数据层的事,多花时间做迁移脚本和备份机制,比多做十个炫酷功能都重要。
第二,AI能力要做减法。能离线完成(情绪规则、标签提取)就不走在线接口,能延迟生成(月度回顾)就不实时阻塞操作。用户每天打开应用的实际体验流畅度,远比“每一次打开都有AI生成结果”更影响留存。
第三,跨设备同步永远要想好冲突策略。我在不同设备上真实使用后发现,冲突概率比预想的高得多。宁可提示用户“内容在其他设备已更新”,也不能悄无声息地覆盖掉某一端的数据。信任感就是在一个个小细节里积攒的。
6.2 智能日记还能往哪个方向扩展
后续空间其实很大,但我的优先级排序是:手势密码和生物识别锁定、日记数据导出为标准格式、按时间维度的情绪趋势曲线、以及基于长期日记数据的个人年度报告。前两项解决的是安全感,后两项解决的是回顾价值。功能不需要一次性堆满,每做一项都要保证它真的被用户高频使用,而不是炫技。
我个人经验是,这类应用最大的复购动力来自用户几个月后翻到旧日记时的那一瞬间惊喜感。你帮用户留住了回忆,用户就离不开你的应用。技术上的深挖,最后都该服务于这个朴素的目标。