背景:一个用"电量"追踪精力的工具
去年做了个 iOS App 叫「元気手账」,核心想法很简单——把自己当成一块电池,每天起始 50% 电量,做了什么事就加减对应的值。比如"-20 开会消耗"、“+15 散步充电”,一天下来精力流向一目了然。
iOS 版用 SwiftData 搞定数据层,写起来很舒服。年初决定出鸿蒙版,用 ArkTS + ArkUI 重写,整个迁移花了三周。这篇主要聊技术迁移过程中踩的坑。
数据模型迁移:从 SwiftData 到 relationalStore
iOS 版的数据模型靠 @Model 宏自动生成 schema,关系用 @Relationship 声明,SwiftData 帮你处理建表、迁移、自动保存。
到鸿蒙这边,@ohos.data.relationalStore 是标准的关系型数据库 API,要自己写建表 SQL、手动管事务。说白了就是从"ORM 全自动"退回到"手写 SQL + 薄封装"。
核心三张表:
- daily_baseline:每天起始电量(默认50)
-
- energy_record:精力变化记录,带正负 delta
-
- project:方舟项目,归类精力投向
我封装了一个 Repository 层来统一管理读写:
- project:方舟项目,归类精力投向
class EnergyRepository {
private store: relationalStore.RdbStore
async insertRecord(record: EnergyRecord): Promise<void> {
const bucket: relationalStore.ValuesBucket = {
'id': record.id,
'title': record.title,
'delta': record.delta,
'timestamp': record.timestamp,
'projectId': record.projectId ?? '',
'isDeleted': 0
}
await this.store.insert('energy_record', bucket)
}
async getDayRecords(dayStart: number, dayEnd: number): Promise<EnergyRecord[]> {
const predicates = new relationalStore.RdbPredicates('energy_record')
predicates.between('timestamp', dayStart, dayEnd)
.equalTo('isDeleted', 0)
.orderByAsc('timestamp')
const result = await this.store.query(predicates)
return this.parseResultSet(result)
}
}
```
iOS 版里 SwiftData 的自动保存、懒加载、变更追踪这些能力全没了,每个操作都得自己写。但好处是掌控感更强,出问题知道去哪查。
## 最恶心的坑:时区与自然日判断
每天的基准电量需要判断"今天是否已经创建过 baseline"。iOS 版一行代码:
```swift
Calendar.current.startOfDay(for: Date())
鸿蒙这边没有直接等价的 API。我最开始用 new Date() 然后 setHours(0,0,0,0) 来算自然日起始,看起来没问题——直到测试发现凌晨 0:30 的记录被归到了前一天。
问题出在 Date 对象默认 UTC,而 setHours 操作的是本地时间但时间戳比较用的是 UTC 毫秒数。最后的解法:
function getDayStartTimestamp(): number {
const now = new Date()
const offsetMs = now.getTimezoneOffset() * 60 * 1000
// 当前本地时间对应的UTC毫秒数,减去当天已过的本地毫秒数
const localMs = now.getTime() - offsetMs
const localDayStart = localMs - (localMs % (24 * 60 * 60 * 1000))
return localDayStart + offsetMs
}
```
说实话这段代码我调了两天,在 UTC+8 和 UTC+5:30(印度时区)下反复验证才确认没问题。教训就是:涉及"自然日"的逻辑,一定要显式处理时区偏移,别信默认行为。
## 快捷动作条:降低记录门槛
做记录工具最怕的事:用户懒得打开。
iOS 版我试过三种交互方案,最后胜出的是底部快捷动作条——预设好常用的记录("-20 开会"、"+10 午休"),一次点击完成,不用填表单。
鸿蒙版用 ArkUI 的横向滚动实现:
```arkts
@Component
struct QuickActionStrip {
@State actions: QuickAction[] = []
build() {
Scroll() {
Row({ space: 12 }) {
ForEach(this.actions, (item: QuickAction) => {
Button(item.title)
.fontSize(14)
.borderRadius(16)
.backgroundColor(item.delta > 0 ? '#E8F5E9' : '#FBE9E7')
.onClick(() => this.commitRecord(item))
})
}.padding({ left: 16, right: 16 })
}
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)
}
}
```
这个交互在鸿蒙上表现挺好,滚动流畅度和 iOS 基本一致。
## 方舟项目:发现精力黑洞
每条记录可以关联一个"方舟项目"——比如写代码、开会、运动。这样按项目维度聚合后就能看出哪件事最耗精力。
我自己用了六周,大概记了 200 多条数据,发现一个反直觉的结论:写技术博客对我是充电行为(平均 +12),而改遗留 bug 才是真正的电量黑洞(平均 -25)。
之前我一直觉得写文章很累,所以总往后排。但数据告诉我它其实是恢复精力的方式。知道这个之后我把改 bug 挪到上午,下午留给写作,整体状态确实好了不少。
项目还有个经验值系统,投入越多等级越高,满级变"传奇"。有点游戏化,但对坚持记录确实有用。
## RDB 批量写入的性能问题
有一天我导入历史数据(大概 80 条),发现明显卡顿。排查后发现是每条 insert 都触发了一次磁盘写入。
解法是用事务包裹批量操作:
```arkts
async batchInsert(records: EnergyRecord[]): Promise<void> {
await this.store.beginTransaction()
try {
for (const record of records) {
const bucket = this.toBucket(record)
await this.store.insert('energy_record', bucket)
}
await this.store.commit()
} catch (e) {
await this.store.rollBack()
throw e
}
}
```
事务包裹后 80 条记录从 2 秒降到了 200ms 左右。这个优化在 SQLite 系的数据库上是常识,但如果你从 SwiftData 过来可能不会第一时间想到——因为 SwiftData 默认就是批量提交的。
## 折叠屏适配
iOS 版对 iPad 做了最大宽度 700pt 居中显示(用 ViewModifier 实现)。鸿蒙折叠屏也需要类似处理,我用 MediaQuery 监听屏幕宽度断点,大于 600vp 时内容区加 `constraintSize({ maxWidth: 600 })`。
这块倒没什么坑,HarmonyOS 的响应式布局文档写得还算清楚。
## 当前状态
元気手账鸿蒙版 1.2.0 已上架华为应用市场,纯本地存储,不联网不注册。下载量还很少,冷启动期。
接下来计划做元服务卡片(桌面直接显示今日剩余电量),以及用 Canvas 组件画周/月精力趋势曲线。
---
有个问题想请教做过鸿蒙本地数据库的同行:relationalStore 在频繁小量写入的场景下(比如用户一天记录 10-15 次,每次单条 insert),你们有观察到明显的 I/O 性能波动吗?我目前单条写入控制在 50ms 内,但不确定数据量上千之后查询性能会不会劣化。
324



被折叠的 条评论
为什么被折叠?



