先说说我为什么做这个
说白了,我想要的只是一个能快速记「+20 跑步」或者「-30 连续开了三小时会」的地方,然后看今天的曲线长什么样。就这么简单。
但找不到合适的。去年下半年我试过各种方法记录状态:日记太重,写一句话要想半天怎么措辞;情绪类 App 总在问我「你现在感受如何」,选项全是词语,什么「焦虑」「平静」「低落」——我不太想每次都给自己贴个情绪标签。找不到,于是开始写第一行代码。
App 的核心逻辑:用「电量」替代「情绪」
这个产品最核心的一个决定,是用「电量」替代「状态」或「情绪」。
电量是中性的,没有好坏判断,只有数字。「今天剩 30%」不会让人有压力,「今天情绪低落」会。而且电量有方向性——可以充、可以耗,天然适合描述精力的流动。
具体的交互模型是这样的:
- 每天从一个基准值(默认 50)出发
-
- 每次记录是一个 delta:
+15 午睡回血、-25 改了一下午 bug
- 每次记录是一个 delta:
-
- 时间轴上看今天所有的涨跌,曲线自然就出来了
-
- 可以把记录挂到「方舟项目」上,追踪某个项目到底有多耗电
方舟项目这个功能是后来才想清楚要加的。有一段时间我同时在跑三个项目,主观感觉「工作量差不多」,但看了一个月的精力日志之后发现,其中一个项目几乎占了我 60% 的消耗。那个项目的会多、决策模糊、沟通成本高——数字出来之前我完全没意识到。
- 可以把记录挂到「方舟项目」上,追踪某个项目到底有多耗电
鸿蒙版的开发经历
这个 App 我先做了 iOS 版,用的是 SwiftUI + SwiftData。跑通核心逻辑之后,我想把它搬到 HarmonyOS 上。
鸿蒙版用的是 ArkTS + ArkUI,工程用 Hvigor 构建。第一周卡在一个很具体的地方:ArkUI 里 @State 装饰的对象,不能直接修改它的某个属性,必须整体替换赋值,否则 UI 不会刷新。iOS 端用 SwiftData 完全没遇到这个限制,我直接写了 this.record.delta = newValue,列表纹丝不动。当时第一反应是数据没写进去,加了几行日志打印,发现数据是对的,值确实变了,UI 就是不动。才意识到是响应式层没感知到变更,翻了一遍 ArkUI 状态管理文档,看到那句「对 @State 修饰的对象属性赋值不会触发 UI 刷新」,才搞清楚原因。前后大概花了一个半小时,有点狼狈。
SwiftUI 的 @State / @Binding 体系跟 ArkUI 的状态装饰器在概念上很像,但这类细节差异不少,写的时候脑子里两套东西容易打架。
iOS 端的精力记录模型用的是 SwiftData 的 @Model,结构大概是这样:
@Model
final class EnergyRecord {
var id: UUID = UUID()
var title: String = ""
var delta: Int = 0 // 正数=补能,负数=耗能
var timestamp: Date = Date()
var isDeleted: Bool = false
var project: Project? // 关联到方舟项目
}
```
`delta` 是核心字段,正负代表方向,绝对值代表量级。整个 App 的计算逻辑都围绕这个字段展开——累加得到当前电量,按时间排序得到曲线数据点。
鸿蒙端没有 SwiftData,`EnergyRecord` 对应的是一个普通 interface,持久化用 `relationalStore`。时间轴渲染用了 `List` + 自定义 `ListItem`,记录添加时的入场动效用 `animateTo` 加弹簧参数:
```arkts
interface EnergyRecord {
title: string
delta: number
timestamp: Date
}
@Entry
@Component
struct EnergyTimeline {
@State records: EnergyRecord[] = []
@State totalEnergy: number = 50
addRecord(title: string, delta: number) {
animateTo({ duration: 300, curve: Curve.EaseOut }, () => {
// 必须整体替换数组,直接 push 不会触发 UI 刷新
this.records = [{ title, delta, timestamp: new Date() }, ...this.records]
this.totalEnergy += delta
})
}
build() {
Column() {
Text(`当前电量: ${this.totalEnergy}`).fontSize(32).fontWeight(FontWeight.Bold)
List() {
ForEach(this.records, (item: EnergyRecord) => {
ListItem() { RecordCard({ record: item }) }
})
}
}
}
}
```
注释里那行是我踩过的坑,顺手留在代码里了。
---
## 那些试了又删掉的东西
**提醒功能**:做完之后用了一周,有一天收到那条推送的时候突然觉得烦——「该记录你的精力了」,我当时的反应是「关你什么事」。当天晚上直接去代码里把它干掉了。这个提醒本身就是一种消耗,违背了这个 App 想要的轻量感。
**精力预测**:想根据历史数据预测今天什么时候会掉电。但样本量不够的时候预测完全不准,有时候给出来的结果反而比没有更让人困惑。砍掉,等数据积累到一定量再说,反正现在加上去只是给自己埋坑。
**社交分享**:「今天的电量曲线」生成图片分享。做了一半停下来,意识到这个产品的调性从一开始就是内向的、自我觉察的,硬要往社交平台上推感觉有点拧。暂时搁置。
现在版本里留下来的功能,每一个都要能回答「这会不会增加记录的门槛」这个问题。
---
## 快捷动作条:把摩擦力降到最低
我做了一个「快捷动作条」,把最常用的几个记录一键完成——比如「-15 刷手机」「+20 散步」,点一下直接记,不用打字。
对应到数据模型是 `QuickAction`,每个快捷动作存了 `title`、`delta`、图标名,以及是否置顶(`isPinned`),置顶的会出现在条的最前面。
鸿蒙版里用的是横向滚动的 `Scroll` + `Row` 布局,配上 HarmonyOS 系统图标。华为手机屏幕普遍大一些,快捷条放更多项目也不会显得拥挤,这个倒是比 iOS 端更好排布。
---
## 现在的进度
1.2.0 版本两周前上线,iOS 端目前大约 60 次下载,鸿蒙版核心的记录和时间轴已经跑通,方舟项目功能还在移植中。
数据这块坚持纯本地存储,没有任何云同步,不需要注册账号。这是我从一开始就定下来的原则——精力记录这种东西,放在自己设备上最合适。
---
## 写给同样在做鸿蒙开发的人
如果你也在做 HarmonyOS App,或者正在把现有产品移植到鸿蒙端,欢迎来聊。
我在移植过程中最费时间的一个坑是 `relationalStore` 的 predicates 条件拼接。它不是链式调用,而是先 new 一个 `RdbPredicates` 对象,然后命令式地往上 `.and()` / `.equalTo()` — 但有一个反直觉的地方:多个条件直接连续调用方法并不是默认 AND 关系,得显式插 `.and()`,否则查询结果会悄无声息地出错,没有任何报错信息,数据就是少了几条。SwiftData 那边写 `#Predicate { $0.delta > 0 && $0.isDeleted == false }` 完全是自然语言,两边一对比,差异相当明显。如果你也在用 `relationalStore` 这套 API,评论区见。
162



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



