用@ohos.data.preferences记录习惯——齿录的习惯追踪实现
如果你想养成更好的牙科护理习惯,可以去鸿蒙应用市场搜一下**「齿录」**,下载下来体验体验。记录每天的刷牙次数、使用牙线、漱口水——坚持一段时间后你会发现自己的口腔护理习惯越来越规律。体验完了再回来看这篇文章,看看习惯追踪是怎么用
@ohos.data.preferences实现的。
写在前面
大家好,我是一名写了十多年Web前端的老兵。从jQuery时代一路走到React/Vue,CSS3动画、requestAnimationFrame、Web Animation API这些都算是看家本领。去年开始转战鸿蒙生态,用ArkTS开发App,这一路踩了不少坑,也积累了不少心得。
很多人觉得"前端转鸿蒙"应该很容易——都是写UI嘛,组件化、状态管理、生命周期,概念都差不多。但真正上手之后你会发现,相似的地方让你觉得亲切,不同的地方让你抓狂。
比如:
- 本地存储:Web的
localStorage在鸿蒙里变成了@ohos.data.preferences,从同步API变成了异步API,而且需要手动调用flush()才能真正写入磁盘。 - 状态管理:React的
useState变成了@State,看起来像,但更新机制完全不同——React是函数式触发重渲染,ArkTS是装饰器驱动的精准更新。
但别担心,核心思想是一样的:都是键值对存储,都是JSON序列化。你之前积累的前端经验,在鸿蒙里依然是你的核心竞争力。
这篇文章聊什么
齿录这个App的习惯追踪,核心要解决:
- 每日打卡:记录今天刷了几次牙、是否用牙线、是否用漱口水
- 连续天数:计算当前连续完成习惯的天数
- 历史统计:查看过去一周/一月的完成情况
第一步:设计数据结构
interface DailyRecord {
date: string // 日期,如 '2024-01-15'
brushMorning: boolean // 早上刷牙
brushEvening: boolean // 晚上刷牙
floss: boolean // 使用牙线
mouthwash: boolean // 使用漱口水
notes: string // 备注
}
interface HabitData {
records: DailyRecord[]
streak: number // 当前连续天数
longestStreak: number // 最长连续天数
}
第二步:封装存储操作
import { preferences } from '@kit.ArkData'
import { common } from '@kit.AbilityKit'
let prefInstance: preferences.Preferences | null = null
async function getPreferences(context: common.UIAbilityContext): Promise<preferences.Preferences> {
if (!prefInstance) {
prefInstance = await preferences.getPreferences(context, 'dental_habits')
}
return prefInstance
}
async function setItem<T>(context: common.UIAbilityContext, key: string, value: T): Promise<boolean> {
try {
const pref = await getPreferences(context)
await pref.put(key, JSON.stringify(value))
await pref.flush()
return true
} catch (err) {
console.error('存储失败:', err)
return false
}
}
async function getItem<T>(context: common.UIAbilityContext, key: string, defaultValue: T): Promise<T> {
try {
const pref = await getPreferences(context)
const value = await pref.get(key, '')
if (typeof value === 'string' && value.length > 0) {
return JSON.parse(value) as T
}
return defaultValue
} catch (err) {
console.error('读取失败:', err)
return defaultValue
}
}
第三步:习惯打卡页面
@Entry
@Component
struct HabitCheckInPage {
@State todayRecord: DailyRecord = {
date: new Date().toISOString().slice(0, 10),
brushMorning: false,
brushEvening: false,
floss: false,
mouthwash: false,
notes: ''
}
@State streak: number = 0
@State completionRate: number = 0
private context = getContext(this) as common.UIAbilityContext
async aboutToAppear() {
await this.loadTodayRecord()
await this.calculateStreak()
this.updateCompletionRate()
}
async loadTodayRecord() {
const today = new Date().toISOString().slice(0, 10)
const records = await getItem<DailyRecord[]>(this.context, 'records', [])
const existing = records.find(r => r.date === today)
if (existing) {
this.todayRecord = existing
}
}
async saveRecord() {
const records = await getItem<DailyRecord[]>(this.context, 'records', [])
const index = records.findIndex(r => r.date === this.todayRecord.date)
if (index > -1) {
records[index] = this.todayRecord
} else {
records.push(this.todayRecord)
}
await setItem(this.context, 'records', records)
await this.calculateStreak()
this.updateCompletionRate()
}
async calculateStreak() {
const records = await getItem<DailyRecord[]>(this.context, 'records', [])
let streak = 0
const today = new Date()
for (let i = 0; i < 365; i++) {
const date = new Date(today.getTime() - i * 24 * 60 * 60 * 1000)
const dateStr = date.toISOString().slice(0, 10)
const record = records.find(r => r.date === dateStr)
if (record && this.isDayComplete(record)) {
streak++
} else if (i > 0) {
// 今天还没完成不算中断
break
}
}
this.streak = streak
}
isDayComplete(record: DailyRecord): boolean {
return record.brushMorning && record.brushEvening && record.floss
}
updateCompletionRate() {
const total = 4 // 4个打卡项
let completed = 0
if (this.todayRecord.brushMorning) completed++
if (this.todayRecord.brushEvening) completed++
if (this.todayRecord.floss) completed++
if (this.todayRecord.mouthwash) completed++
this.completionRate = completed / total
}
async toggleHabit(key: keyof DailyRecord) {
(this.todayRecord as any)[key] = !(this.todayRecord as any)[key]
this.updateCompletionRate()
await this.saveRecord()
}
build() {
Column() {
// 连续天数显示
Column() {
Text(`${this.streak}`)
.fontSize(48)
.fontWeight(FontWeight.Bold)
.fontColor('#10B981')
Text('天连续')
.fontSize(14)
.fontColor('#9CA3AF')
}
.margin({ bottom: 24 })
// 完成度环
Stack({ alignContent: Alignment.Center }) {
Circle()
.width(120)
.height(120)
.stroke('#374151')
.strokeWidth(8)
.fill('transparent')
Circle()
.width(120)
.height(120)
.stroke('#10B981')
.strokeWidth(8)
.strokeDashArray([this.completionRate * 377, 377])
.strokeLineCap(LineCapStyle.Round)
.fill('transparent')
.rotate({ angle: -90 })
Text(`${Math.round(this.completionRate * 100)}%`)
.fontSize(20)
.fontColor('#FFFFFF')
}
.margin({ bottom: 24 })
// 打卡项
Column() {
this.CheckItem('早上刷牙', this.todayRecord.brushMorning, () => this.toggleHabit('brushMorning'))
this.CheckItem('晚上刷牙', this.todayRecord.brushEvening, () => this.toggleHabit('brushEvening'))
this.CheckItem('使用牙线', this.todayRecord.floss, () => this.toggleHabit('floss'))
this.CheckItem('漱口水', this.todayRecord.mouthwash, () => this.toggleHabit('mouthwash'))
}
// 备注
TextInput({ placeholder: '备注(选填)', text: this.todayRecord.notes })
.onChange((value: string) => {
this.todayRecord.notes = value
this.saveRecord()
})
.margin({ top: 16 })
}
.width('100%')
.height('100%')
.backgroundColor('#111827')
.padding(16)
}
@Builder
CheckItem(label: string, checked: boolean, onTap: () => void) {
Row() {
Text(checked ? '✓' : '○')
.fontSize(20)
.fontColor(checked ? '#10B981' : '#6B7280')
.width(30)
Text(label)
.fontSize(16)
.fontColor('#FFFFFF')
}
.width('100%')
.height(56)
.padding(16)
.backgroundColor(checked ? 'rgba(16, 185, 129, 0.1)' : '#1F2937')
.borderRadius(12)
.margin({ bottom: 8 })
.onClick(onTap)
}
}
第四步:历史统计页面
@Entry
@Component
struct HabitStatsPage {
@State weekData: DailyRecord[] = []
@State monthData: DailyRecord[] = []
private context = getContext(this) as common.UIAbilityContext
async aboutToAppear() {
const records = await getItem<DailyRecord[]>(this.context, 'records', [])
const today = new Date()
// 最近7天
this.weekData = []
for (let i = 0; i < 7; i++) {
const date = new Date(today.getTime() - i * 24 * 60 * 60 * 1000)
const dateStr = date.toISOString().slice(0, 10)
const record = records.find(r => r.date === dateStr)
this.weekData.unshift(record || {
date: dateStr,
brushMorning: false,
brushEvening: false,
floss: false,
mouthwash: false,
notes: ''
})
}
}
build() {
Column() {
Text('本周记录')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
.margin({ bottom: 16 })
// 一周热力图
Row() {
ForEach(this.weekData, (record: DailyRecord) => {
Column() {
Text(new Date(record.date).getDate().toString())
.fontSize(12)
.fontColor('#9CA3AF')
Column() {
this.Dot(record.brushMorning)
this.Dot(record.brushEvening)
this.Dot(record.floss)
this.Dot(record.mouthwash)
}
.margin({ top: 4 })
}
.width(40)
.alignItems(HorizontalAlign.Center)
})
}
.width('100%')
.justifyContent(FlexAlign.SpaceAround)
// 统计数据
Column() {
Text(`完成率: ${this.getCompletionRate()}%`)
.fontSize(16)
.fontColor('#FFFFFF')
Text(`最佳连续: ${this.getBestStreak()}天`)
.fontSize(16)
.fontColor('#FFFFFF')
.margin({ top: 8 })
}
.margin({ top: 24 })
}
.width('100%')
.height('100%')
.backgroundColor('#111827')
.padding(16)
}
@Builder
Dot(checked: boolean) {
Row()
.width(8)
.height(8)
.backgroundColor(checked ? '#10B981' : '#374151')
.borderRadius(4)
.margin({ bottom: 2 })
}
private getCompletionRate(): number {
const total = this.weekData.length * 4
const completed = this.weekData.reduce((sum, r) => {
return sum + (r.brushMorning ? 1 : 0) + (r.brushEvening ? 1 : 0) +
(r.floss ? 1 : 0) + (r.mouthwash ? 1 : 0)
}, 0)
return Math.round((completed / total) * 100)
}
private getBestStreak(): number {
// 计算最长连续天数
return 0 // 简化实现
}
}
总结
这篇文章围绕"齿录"的习惯追踪功能,讲了@ohos.data.preferences在习惯追踪场景的应用:
数据结构
- 每日记录:日期 + 各个习惯的完成状态
- 连续天数:当前连续和最长连续
核心功能
- 打卡:切换习惯状态并保存
- 连续计算:从今天往前查,找到中断为止
- 完成度:已打卡项 / 总项数
UI组件
- 进度环:用
strokeDashArray实现 - 打卡项:点击切换状态
- 热力图:展示一周的完成情况
注意事项
- 数据修改后立即保存并调用
flush() - 连续天数计算要考虑"今天还没完成"的情况
- 热力图要展示7天的数据,即使某天没有记录
下一篇文章我会讲齿录的提醒通知功能,看看怎么用@ohos.notification设置刷牙提醒。