鸿蒙原生开发——从零构建习惯养成追踪器
一、引言
市面上的习惯追踪 App 不少,但如果你想自己动手在鸿蒙上做一个呢?这篇教程就是你的起点。
我们将从零开始,用 ArkUI 构建一个完整的**“习惯养成追踪器”**——不是"演示某个组件怎么用"的玩具 Demo,而是一个真正可用的功能模块:查看本周 7 天的习惯完成情况、点击日期格子切换完成状态、添加自定义习惯、查看连续打卡天数和完成率。所有数据当前存储在页面状态中(后续可以接入数据库,那就是一个真正的 App 了)。
本文属于**“鸿蒙原生开发”**系列——关注的是"如何用 ArkUI 从零构建一个完整功能",而非单独介绍某个新 API。你将学到的是组合多个组件解决实际问题的能力。
阅读完本文,你将能够:
- 用嵌套
ForEach构建"习惯 × 星期"的二维网格 - 实现不可变状态更新,正确处理
@State数组 - 设计暗色主题下的视觉层次(卡片、颜色标签、进度条)
- 实现自定义弹窗(图标选择器 + 文本输入)
- 计算连续打卡天数(streak)和每周完成率
- 构建一个完整、美观、可交互的原生应用功能
二、功能设计
2.1 需求梳理
一个习惯追踪器需要哪些功能?我们把它拆成四个用户故事:
- 查看本周习惯:用户打开页面,看到自己正在追踪的所有习惯,以及每个习惯在本周 7 天(周一至周日)的完成情况。已完成的天用彩色方块标记,未完成的天用暗色方块标记。
- 切换完成状态:用户点击某一天的格子,完成 ↔ 未完成之间切换。这是最高频的操作——用户每天打卡时需要快速标记。
- 查看统计数据:每个习惯显示"连续打卡天数"(从今天往回数,一旦中断就停止计数)和"本周完成率"(完成天数 / 7)。页面顶部显示总体完成率。
- 管理习惯:用户可以添加新习惯(选择图标和命名),也可以删除不再追踪的习惯。
这四点对应了 Demo 的 4 个交互点:切换状态、查看统计、添加习惯、删除习惯。
2.2 数据结构
核心数据结构是一个 HabitInfo 数组:
interface HabitInfo {
id: number; // 唯一 ID
name: string; // 习惯名称
icon: string; // emoji 图标
color: string; // 主题色(蓝色/绿色/红色/紫色...)
week: boolean[]; // 7 天完成状态 [周一, 周二, ..., 周日]
}
week 是一个长度为 7 的布尔数组,week[0] 代表周一,week[6] 代表周日。这个设计简单直观——判断"周三是否完成"只需要检查 habit.week[2]。
为什么用固定 7 天的数组而不是 Map<日期, boolean>?因为习惯追踪天然以"周"为单位——用户关心的是"这周我坚持得怎么样",而不是某个特定日期的打卡。固定数组也简化了 UI 渲染——7 个格子刚好铺满一行,不需要考虑滚动。
三、核心功能实现
3.1 每周视图:跨星期计算
页面顶部需要显示"本周"的日期范围,如"6/9 - 6/15"。这需要计算本周一的日期:
getMondayDate(): string {
const today = new Date();
const dow = today.getDay(); // 0=周日, 1=周一, ..., 6=周六
const offset = dow === 0 ? 6 : dow - 1;
const mon = new Date(today.getFullYear(), today.getMonth(), today.getDate() - offset);
const sun = new Date(mon.getFullYear(), mon.getMonth(), mon.getDate() + 6);
return `${mon.getMonth() + 1}/${mon.getDate()} - ${sun.getMonth() + 1}/${sun.getDate()}`;
}
getDay() 返回 0(周日)到 6(周六),而我们的 week 数组是周一索引。所以当 dow === 0(今天是周日),周一在当前日期之前 6 天;其他情况,周一在当前日期之前 dow - 1 天。
getTodayIndex() 方法返回今天在 week 数组中的位置,用于高亮"今天"这一列:
getTodayIndex(): number {
const dow = new Date().getDay();
return dow === 0 ? 6 : dow - 1; // 周日 → 6, 周一 → 0
}
3.2 连续打卡天数(Streak)
"连续打卡 N 天"是习惯追踪 App 的灵魂功能。计算逻辑:
streakCount(habit: HabitInfo): number {
let count = 0;
const todayIdx = this.getTodayIndex();
for (let d = todayIdx; d >= 0; d--) {
if (habit.week[d]) count++;
else break; // 一旦中断,停止计数
}
return count;
}
从今天往回扫描,如果某一天没完成(false),立即停止。比如:
- 周一到周三完成,周四未完成,周五完成 →
streak = 1(只算周五) - 周三到周五完成,周六周日未完成 →
streak = 3(周五、周四、周三连续)
这里有一个设计决策:为什么 streak 只回溯到本周一?如果用户上周五、周六、周日都完成了,本周一也完成了,streak 应该是 4 还是 1?
在我们的 Demo 中,streak 只统计本周范围,所以答案是 1。这是 Demo 的简化——生产级 App 需要跨周追踪,通常需要存一个时间戳而非布尔数组。我们在"进阶方向"一节会讨论这一点。
3.3 切换完成状态:不可变更新
用户点击某个日期格子时,需要翻转该习惯该天的完成状态:
toggleDay(habitIdx: number, dayIdx: number): void {
const newHabits: HabitInfo[] = [];
for (let h = 0; h < this.habits.length; h++) {
const habit = this.habits[h];
if (h === habitIdx) {
const newWeek: boolean[] = [];
for (let d = 0; d < 7; d++) {
newWeek.push(d === dayIdx ? !habit.week[d] : habit.week[d]);
}
newHabits.push({
id: habit.id, name: habit.name,
icon: habit.icon, color: habit.color, week: newWeek
});
} else {
newHabits.push(habit); // 未变动的习惯保留原引用
}
}
this.habits = newHabits;
}
这段代码看起来很长,但逻辑非常清晰:遍历所有习惯 → 找到被点击的那个 → 创建新的 week 数组(只翻转目标天)→ 创建新的 HabitInfo 对象 → 替换整个 habits 数组。
ArkUI 的 @State 要求替换引用才能触发重绘。直接修改属性(如 this.habits[0].week[2] = true)不会触发 UI 更新。这个不可变更新的模式在前面的系列文章中已经多次出现,但它确实是 ArkUI 开发中最需要牢记的规则。
3.4 添加习惯:自定义弹窗
添加习惯需要一个弹窗让用户选择图标和输入名称。在 ArkUI 中,可以用条件渲染实现:
if (this.showDialog) {
Column() {
// 半透明背景遮罩
}
.backgroundColor('#00000088')
.onClick(() => { this.showDialog = false; }) // 点击遮罩关闭
}
弹窗内容包含:
图标选择器:8 个 emoji 图标(🏃📖💧🧘🎸💻🍎✍️)排列成一行,当前选中的图标有蓝色边框和微蓝背景。用户点击一个图标 → this.newIcon 更新 → 边框和背景立即切换。
名称输入框:使用 TextInput 组件,支持 placeholder 提示文字。onChange 回调实时更新 this.newName。
操作按钮:取消(关闭弹窗)和确定添加(调用 addHabit)。确定按钮在输入为空时呈灰色不可用态,输入文字后变为蓝色可用态——这是一个微妙的 UX 细节,提示用户"你需要先输入名称"。
3.5 删除习惯
每个习惯行右侧有一个半透明的 🗑 图标。点击时调用 deleteHabit:
deleteHabit(idx: number): void {
const newHabits: HabitInfo[] = [];
for (let h = 0; h < this.habits.length; h++) {
if (h !== idx) newHabits.push(this.habits[h]);
}
this.habits = newHabits;
}
这里再次使用了不可变更新——创建新数组、替换原引用。删除操作使用 !== 跳过目标索引,实际上是"过滤掉"而非"移除"。
删除按钮的半透明设计(#FFFFFF22,仅 13% 不透明度)让它"存在但低调"——用户通常不会误触它,但需要时能找到。这是一种"温和的破坏性操作"设计:不要太显眼(避免误触),也不要太隐蔽(用户需要时能找到)。
3.6 统计展示
页面顶部显示总体完成率:
totalWeekDone(): number {
let total = 0;
for (const habit of this.habits) {
total += this.weekDone(habit);
}
const max = this.habits.length * 7;
return max === 0 ? 0 : Math.round((total / max) * 100);
}
每个习惯右侧还有单独的进度条——40vp 宽的细条,已完成的百分比部分用习惯主题色填充,未完成部分用暗灰色。这个迷你进度条让用户一眼就能判断"哪个习惯坚持得最好"。
四、UI 设计
4.1 暗色主题
页面采用深色主题(#1a1a2e 深海军蓝背景),这是个人效率类 App 的热门选择——深色背景让人专注,彩色元素(习惯颜色、进度条)在深色背景上对比度更强。
色彩层次:
- #1a1a2e — 页面背景(最深)
- #1e1e36 — 习惯卡片背景(稍亮,形成分层)
- #2a2a3a — 未完成日期格子(与卡片背景区分)
- #FFFFFF / #FFFFFF66 / #FFFFFF44 / #FFFFFF22 — 四级白色文字透明度梯度
四种白色透明度对应了信息层级:标题 100%、名称 100%、周日期标签 40%(#FFFFFF66)、统计标签 27%(#FFFFFF44)、删除按钮 13%(#FFFFFF22)。越重要的信息越"亮",越次要的信息越"暗"——用户无需思考就知道该看哪里。
4.2 颜色编码
每个习惯分配一个主题色(从 7 种颜色中按添加顺序循环)。已完成日期格子的背景色使用该习惯的主题色——蓝色的习惯打出蓝色的 ✓,绿色的习惯打出绿色的 ✓。
这种颜色编码让用户在看整体页面时,不用读文字就能区分不同习惯——蓝色方块属于"晨跑",绿色方块属于"阅读"。颜色成为一种无意识的视觉线索。
4.3 今天高亮
星期导航栏中,"今天"对应列有金色文字(#FF9F0A)和半透明金色圆形背景(#FF9F0A22)。日期格子中,"今天"那一列有 1.5vp 的白色半透明边框。
双层高亮让用户快速定位——“今天"在 7 列中很醒目。这对习惯追踪尤为重要,因为用户打卡时最关心的是"今天我完成了没”。
4.4 添加按钮
列表底部的"+ 添加新习惯"按钮使用了虚线边框(BorderStyle.Dashed),而不是实线。虚线边框传递的信号是"这里还可以填充更多内容"——它看起来像一个占位符,而不是一个确定的按钮。
这是 UI 设计中"示能(Affordance)"的一个例子:线的形状暗示了操作的性质——虚线 = “不完整、可添加”,实线 = “确定的、已完成的”。
五、完整代码结构
HabitTrackerPage
├── Stack(根容器)
│ ├── Column(主界面)
│ │ ├── Row(标题栏:习惯养成 + 本周日期 + 总体完成率)
│ │ ├── Row(星期导航栏,今天高亮)
│ │ └── Scroll(习惯列表 + 添加按钮)
│ │ ├── ForEach habits → Row(习惯卡片)
│ │ │ ├── Column(图标 + 名称 + 连续天数)
│ │ │ ├── Row(7 天格子 + 删除按钮)
│ │ │ └── Column(7/N 计数 + 迷你进度条)
│ │ └── Row(+ 添加新习惯 虚线按钮)
│ └── if showDialog → Column(遮罩 + 弹窗)
│ ├── Row(8 个图标选择)
│ ├── TextInput(习惯名称输入)
│ └── Row(取消 + 确定添加)
六、进阶方向
当前 Demo 是一个完整的前端功能模块,但如果要变成一个真正的 App,有几个扩展方向:
-
数据持久化:接入鸿蒙的 Preferences 或 RelationalStore,将习惯数据保存到本地。这样关闭 App 再打开,数据不会丢失。
-
跨周累加:目前 streak 只统计本周。扩展为跨周连续计算需要将
week改为按日期索引的Map<string, boolean>或存储打卡时间戳数组。 -
提醒通知:结合鸿蒙的 Notification API,在用户设定的时间发送提醒(如"该晨跑了!")。
-
数据可视化:添加月度热力图(类似 GitHub 贡献图),展示长期坚持情况。可以用 Canvas 或 Grid 实现。
-
习惯模板库:预设一些常用习惯(运动、阅读、冥想、早起等),让用户一键添加。
七、总结
本文从零构建了一个完整的习惯养成追踪器——不是演示某个 API,而是用 ArkUI 解决一个实际问题。回顾覆盖的核心技术:
-
嵌套 ForEach 构建二维网格:外层遍历习惯,内层遍历 7 天。每个格子独立响应点击。
-
不可变状态更新:
@State驱动 UI 渲染,每次状态变更必须替换整个引用链(week→HabitInfo→habits[])。未变动的对象保留原引用(性能优化),变动的对象创建新引用(触发重绘)。 -
暗色主题 + 色彩编码:四级白色透明度建立信息层级,7 种习惯色实现无意识视觉区分,金色高亮标记"今天"。
-
Streak 计算:从今天往回扫描,遇到中断即停止。简单高效,O(n) 复杂度。
-
自定义弹窗:条件渲染 + 半透明遮罩 + 图标选择器 + TextInput + 双按钮。弹窗点击遮罩可关闭,输入为空时确认按钮不可用。
-
迷你进度条:用嵌套 Row + 百分比宽度实现简单进度条,颜色与习惯主题色保持一致。
"鸿蒙原生开发"系列关注的是——如何把多个 ArkUI 组件和技术点组合成真正可用的功能。习惯追踪器是一个很好的起点:它不需要后端、不需要权限、不需要网络请求,但包含了完整的 CRUD 操作、数据统计、视觉设计——麻雀虽小,五脏俱全。
更多推荐




所有评论(0)