HarmonyOS ArkTS 实战:实现一个饮水提醒应用
HarmonyOS ArkTS 实战:实现一个饮水提醒应用
项目效果
本文实现的是一个基于 HarmonyOS 和 ArkTS 的饮水提醒应用。项目中使用 ArkUI 组件完成页面布局,通过 @State 管理状态数据,实现一键喝水记录、自定义饮水量、每日目标设置、饮水进度、饮水历史、今日统计和提醒设置等功能。
最终运行效果如下:

页面主要包含以下内容:
- 顶部应用标题和今日统计;
- 圆形饮水进度显示;
- 快捷喝水按钮(200ml/300ml/500ml);
- 自定义饮水量输入;
- 每日目标设置;
- 饮水记录列表;
- 删除记录功能;
- 空状态提示;
- 页面整体采用 ArkUI 声明式布局。
本文重点是演示如何在 HarmonyOS 项目中使用 ArkTS 和 ArkUI 实现一个健康管理类单页面应用。项目代码主要写在 entry/src/main/ets/pages/Index.ets 文件中,适合作为 HarmonyOS ArkTS 入门到进阶之间的练习案例。
前言
水是生命之源,保持充足的饮水对健康至关重要。很多人因为工作忙碌经常忘记喝水,一个简单的饮水提醒应用可以帮助我们养成按时喝水的好习惯,追踪每日饮水量,确保达到健康目标。
从应用开发角度来看,这个项目不依赖后端接口,也不需要数据库,但能练习 ArkTS 中的状态管理、按钮交互、圆形进度、列表渲染、数据统计和数组操作等内容。
本文基于 HarmonyOS 和 ArkTS 实现一个饮水提醒应用。用户可以一键记录喝水量,设置每日饮水目标,查看饮水进度,管理饮水记录,追踪自己的饮水习惯。
这个项目的核心不是简单的数字累加,而是帮助用户养成健康饮水习惯的完整流程。每一次喝水记录、目标调整和进度更新,本质上都是对饮水数据的管理。状态变化后,页面会自动刷新,这正是 ArkUI 声明式开发的基本思想。
一、项目目标
本次实践主要实现以下目标:
- 创建 HarmonyOS ArkTS 页面;
- 使用
@Entry和@Component定义页面组件; - 使用
@State管理页面状态; - 实现快捷喝水按钮;
- 支持自定义饮水量;
- 可设置每日饮水目标;
- 圆形进度条显示完成进度;
- 记录每次饮水的时间和量;
- 统计今日饮水量;
- 计算完成百分比;
- 支持删除饮水记录;
- 使用
List和ForEach渲染饮水记录; - 使用空状态提示优化无记录页面;
- 使用
@Builder封装统计卡片、快捷按钮和记录项; - 完成一个可以运行的饮水提醒页面。
这个项目虽然是单页面应用,但它有完整的记录、统计、进度展示和记录管理流程,比普通静态页面更适合练习 ArkTS。
二、技术栈
| 类型 | 内容 |
|---|---|
| 开发方向 | HarmonyOS 应用开发 |
| 开发语言 | ArkTS |
| UI 框架 | ArkUI |
| SDK 版本 | HarmonyOS API 23 及以上 |
| 工程模型 | Stage 模型 |
| 核心组件 | Text / TextInput / Button / Column / Row / Circle / Stack / List / ForEach |
| 状态管理 | @State |
| 数据处理 | 进度计算 / 数组统计 / 时间格式化 |
| 项目入口 | entry/src/main/ets/pages/Index.ets |
| 运行平台 | 模拟器或真机 |
本项目是 HarmonyOS 原生 ArkTS 项目。页面主体由 ArkUI 组件构建,核心逻辑写在 Index.ets 文件中,不依赖后端接口,也不需要额外配置数据库。
三、为什么选择饮水提醒项目
饮水提醒应用适合作为 ArkTS 练习项目,主要有以下几个原因。
第一,实用性强。健康饮水是每个人的需求,一个简洁的饮水记录工具可以直接应用于日常生活,帮助用户保持健康。
第二,业务场景真实。办公室人群经常忘记喝水,需要这样一个简单的工具来提醒和记录饮水。
第三,适合练习状态管理。饮水量、每日目标、饮水记录都属于页面状态。状态变化后,页面会自动刷新。
第四,适合练习圆形进度。圆形进度条可以直观展示饮水完成情况,视觉效果好。
第五,适合练习快速交互。一键喝水按钮是非常经典的快捷交互形式。
第六,扩展空间比较大。基础功能完成后,可以继续增加本地存储、定时提醒、饮水历史统计、图表分析和健康建议。
在本项目中,records 是核心数据源。页面中的统计数据、进度显示和记录列表都由它计算或渲染得到。
四、功能规则说明
快捷饮水量选项:
| 按钮 | 饮水量 | 适用场景 |
|---|---|---|
| 小杯 | 200ml | 小杯水 |
| 中杯 | 300ml | 普通杯子 |
| 大杯 | 500ml | 大瓶水 |
统计规则如下:
今日饮水量 = 今日所有饮水记录的量之和
完成率 = 今日饮水量 / 每日目标 * 100%
剩余量 = 每日目标 - 今日饮水量(最小为0)
默认每日目标为2000ml,用户可以自定义调整。
每次添加或删除饮水记录后,统计数据和进度都会自动更新。
五、项目结构
本项目主要修改首页文件:
entry
└── src
└── main
└── ets
└── pages
└── Index.ets
其中:
| 文件 | 作用 |
|---|---|
| Index.ets | 编写页面结构、状态数据和饮水记录逻辑 |
本文不涉及复杂路由,也不需要额外创建多个页面。对于练习项目来说,把主要逻辑集中在一个 Index.ets 文件中更方便理解。
如果后续继续扩展,可以考虑把进度圆环、快捷按钮、记录项和设置区域拆分成独立组件。
六、核心实现思路
本项目的核心流程如下:
- 定义饮水记录数据结构;
- 使用
@State保存每日目标、自定义饮水量和饮水记录; - 用户点击快捷按钮添加对应量的饮水;
- 用户也可以输入自定义饮水量;
- 使用数组保存所有饮水记录;
- 圆形进度条显示今日完成进度;
- 统计今日饮水量和完成率;
- 使用
ForEach渲染饮水记录列表; - 点击删除按钮删除指定记录;
- 用户可以调整每日目标;
- 没有记录时显示空状态提示。
项目中最重要的状态变量如下:
@State dailyGoal: number = 2000
@State customAmount: string = ''
@State today: string = ''
@State records: WaterRecord[] = []
@State nextId: number = 1
其中:
| 状态变量 | 作用 |
|---|---|
| dailyGoal | 每日饮水目标(ml) |
| customAmount | 自定义饮水量输入 |
| today | 今日日期 |
| records | 饮水记录数组 |
| nextId | 生成记录唯一编号 |
records 是本项目中最重要的数据。所有进度计算、统计数据和记录列表都依赖这个数组。
七、Index.ets 完整代码
打开文件:
entry/src/main/ets/pages/Index.ets
将其中内容替换为下面代码:
interface WaterRecord {
id: number
amount: number
time: string
date: string
}
@Entry
@Component
struct Index {
@State dailyGoal: number = 2000
@State customAmount: string = ''
@State nextId: number = 4
@State today: string = this.getTodayString()
@State records: WaterRecord[] = [
{ id: 1, amount: 300, time: '09:00', date: this.getTodayString() },
{ id: 2, amount: 200, time: '11:30', date: this.getTodayString() },
{ id: 3, amount: 500, time: '14:00', date: this.getTodayString() }
]
private quickAmounts: number[] = [200, 300, 500]
aboutToAppear() {
this.today = this.getTodayString()
}
private addWater(amount: number): void {
if (amount <= 0) return
let record: WaterRecord = {
id: this.nextId,
amount: amount,
time: this.getCurrentTime(),
date: this.today
}
this.records = [record, ...this.records]
this.nextId++
this.customAmount = ''
}
private addCustomWater(): void {
let amount = parseInt(this.customAmount)
if (!isNaN(amount) && amount > 0) {
this.addWater(amount)
}
}
private deleteRecord(id: number): void {
this.records = this.records.filter((r: WaterRecord) => r.id !== id)
}
private getTodayTotal(): number {
return this.records
.filter((r: WaterRecord) => r.date === this.today)
.reduce((sum: number, r: WaterRecord) => sum + r.amount, 0)
}
private getProgress(): number {
return Math.min(this.getTodayTotal() / this.dailyGoal, 1)
}
private getPercentage(): number {
return Math.round(this.getProgress() * 100)
}
private getRemaining(): number {
return Math.max(this.dailyGoal - this.getTodayTotal(), 0)
}
private getTodayString(): string {
return this.formatDate(new Date())
}
private getCurrentTime(): string {
let date = new Date()
return `${this.formatTwo(date.getHours())}:${this.formatTwo(date.getMinutes())}`
}
private formatDate(date: Date): string {
return `${date.getFullYear()}-${this.formatTwo(date.getMonth() + 1)}-${this.formatTwo(date.getDate())}`
}
private formatTwo(value: number): string {
return value < 10 ? '0' + value.toString() : value.toString()
}
private adjustGoal(delta: number): void {
let newGoal = this.dailyGoal + delta
if (newGoal >= 500 && newGoal <= 5000) {
this.dailyGoal = newGoal
}
}
@Builder
StatCard(title: string, value: string, unit: string, color: string) {
Column() {
Row() {
Text(value)
.fontSize(22)
.fontWeight(FontWeight.Bold)
.fontColor(color)
Text(unit)
.fontSize(13)
.fontColor(color)
.margin({ left: 4, top: 6 })
}
Text(title)
.fontSize(13)
.fontColor('#6B7280')
.margin({ top: 4 })
}
.width('31%')
.height(75)
.justifyContent(FlexAlign.Center)
.backgroundColor(Color.White)
.borderRadius(16)
.shadow({
radius: 10,
color: '#12000000',
offsetX: 0,
offsetY: 4
})
}
@Builder
QuickButton(amount: number) {
Button(`${amount}ml`)
.height(48)
.layoutWeight(1)
.fontSize(16)
.fontColor(Color.White)
.backgroundColor('#3B82F6')
.borderRadius(24)
.onClick(() => {
this.addWater(amount)
})
}
@Builder
RecordItem(record: WaterRecord) {
Row() {
Text('💧')
.fontSize(24)
.width(40)
.height(40)
.textAlign(TextAlign.Center)
.backgroundColor('#EFF6FF')
.borderRadius(20)
Column() {
Text(`${record.amount}ml`)
.fontSize(16)
.fontWeight(FontWeight.Medium)
.fontColor('#182431')
Text(record.time)
.fontSize(13)
.fontColor('#6B7280')
.margin({ top: 4 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
.margin({ left: 12 })
Button('删除')
.height(32)
.fontSize(12)
.fontColor('#EF4444')
.backgroundColor('#FEF2F2')
.borderRadius(14)
.onClick(() => {
this.deleteRecord(record.id)
})
}
.width('100%')
.padding(14)
.margin({ bottom: 10 })
.backgroundColor(Color.White)
.borderRadius(12)
.shadow({
radius: 6,
color: '#08000000',
offsetX: 0,
offsetY: 2
})
}
build() {
Column() {
Text('饮水提醒')
.fontSize(28)
.fontWeight(FontWeight.Bold)
.fontColor('#182431')
.margin({ top: 22 })
Text('基于 HarmonyOS ArkTS 的健康饮水助手')
.fontSize(14)
.fontColor('#6B7280')
.margin({ top: 8, bottom: 20 })
Row() {
this.StatCard('已饮水', `${this.getTodayTotal()}`, 'ml', '#3B82F6')
this.StatCard('完成率', `${this.getPercentage()}`, '%', '#10B981')
this.StatCard('还需喝', `${this.getRemaining()}`, 'ml', '#F59E0B')
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
.margin({ bottom: 24 })
Stack() {
Circle()
.width(220)
.height(220)
.fill('#EFF6FF')
.stroke('#DBEAFE')
.strokeWidth(16)
Circle()
.width(220)
.height(220)
.fill(Color.Transparent)
.stroke('#3B82F6')
.strokeWidth(16)
.strokeDashArray([2 * Math.PI * 102 * this.getProgress(), 2 * Math.PI * 102])
.strokeDashOffset(2 * Math.PI * 102 * 0.25)
.animation({ duration: 500, curve: Curve.EaseInOut })
Column() {
Text('💧')
.fontSize(40)
.margin({ bottom: 8 })
Text(`${this.getTodayTotal()}ml`)
.fontSize(36)
.fontWeight(FontWeight.Bold)
.fontColor('#3B82F6')
Text(`目标 ${this.dailyGoal}ml`)
.fontSize(14)
.fontColor('#6B7280')
.margin({ top: 4 })
}
}
.width(240)
.height(240)
.margin({ bottom: 24 })
.justifyContent(FlexAlign.Center)
.alignItems(Alignment.Center)
Text('快速记录')
.fontSize(16)
.fontWeight(FontWeight.Medium)
.fontColor('#182431')
.width('100%')
.margin({ bottom: 12 })
Row() {
this.QuickButton(200)
Blank().width(10)
this.QuickButton(300)
Blank().width(10)
this.QuickButton(500)
}
.width('100%')
.margin({ bottom: 16 })
Row() {
TextInput({
placeholder: '自定义饮水量(ml)',
text: this.customAmount
})
.layoutWeight(1)
.height(44)
.fontSize(15)
.backgroundColor('#F5F7FA')
.borderRadius(22)
.padding({ left: 16, right: 16 })
.type(InputType.Number)
.onChange((value: string) => {
this.customAmount = value
})
Button('添加')
.height(44)
.margin({ left: 10 })
.padding({ left: 20, right: 20 })
.fontSize(15)
.fontColor(Color.White)
.backgroundColor('#3B82F6')
.borderRadius(22)
.onClick(() => {
this.addCustomWater()
})
}
.width('100%')
.margin({ bottom: 16 })
Row() {
Text('每日目标')
.fontSize(14)
.fontColor('#6B7280')
Blank()
Button('-')
.width(32)
.height(32)
.fontSize(18)
.fontColor('#4B5563')
.backgroundColor('#F3F4F6')
.borderRadius(16)
.onClick(() => { this.adjustGoal(-100) })
Text(`${this.dailyGoal}ml`)
.fontSize(16)
.fontWeight(FontWeight.Medium)
.margin({ left: 12, right: 12 })
Button('+')
.width(32)
.height(32)
.fontSize(18)
.fontColor('#4B5563')
.backgroundColor('#F3F4F6')
.borderRadius(16)
.onClick(() => { this.adjustGoal(100) })
}
.width('100%')
.padding(14)
.backgroundColor(Color.White)
.borderRadius(12)
.margin({ bottom: 16 })
Text('今日记录')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#182431')
.width('100%')
.margin({ bottom: 12 })
if (this.records.filter(r => r.date === this.today).length === 0) {
Column() {
Text('还没有饮水记录')
.fontSize(16)
.fontColor('#6B7280')
Text('点击上方按钮,开始记录第一杯水吧')
.fontSize(13)
.fontColor('#9CA3AF')
.margin({ top: 8 })
}
.width('100%')
.padding(30)
.backgroundColor(Color.White)
.borderRadius(16)
} else {
List() {
ForEach(this.records.filter((r: WaterRecord) => r.date === this.today), (record: WaterRecord) => {
ListItem() {
this.RecordItem(record)
}
}, (record: WaterRecord) => record.id.toString())
}
.width('100%')
.layoutWeight(1)
.scrollBar(BarState.Off)
}
}
.width('100%')
.height('100%')
.padding({ left: 18, right: 18 })
.backgroundColor('#F5F7FA')
}
}
八、代码实现说明
1. 定义饮水记录数据结构
项目中使用 WaterRecord 接口描述每一次饮水记录:
interface WaterRecord {
id: number
amount: number
time: string
date: string
}
字段说明如下:
| 字段 | 作用 |
|---|---|
| id | 记录唯一编号 |
| amount | 饮水量(ml) |
| time | 饮水时间 |
| date | 饮水日期 |
2. 使用 @State 管理页面状态
本项目中使用多个状态变量保存每日目标、自定义饮水量和饮水记录。当用户点击快捷按钮、添加自定义饮水或删除记录时,状态数据都会发生变化,页面自动刷新。
3. 添加饮水记录
点击快捷按钮或添加自定义饮水时,创建新的记录并添加到数组开头。新记录包含当前时间和日期。
4. 圆形进度显示
使用两个Circle组件实现圆形进度条,通过strokeDashArray控制进度长度,并有平滑动画效果。进度值根据今日饮水量和每日目标计算。
5. 统计数据计算
今日饮水量通过filter筛选今日记录后用reduce求和,完成率和剩余量都基于此动态计算。
6. 每日目标调整
用户可以通过+/-按钮调整每日目标,范围500ml到5000ml,每次调整100ml。
7. 删除记录
使用唯一id删除记录,删除后统计数据和进度自动更新。
九、运行项目
代码编写完成后,在DevEco Studio中运行项目。测试快捷喝水、自定义饮水、目标调整、删除记录等功能,确保进度和统计正确更新。
十、开发中遇到的问题
- 圆形进度条起始角度调整,确保从顶部开始
- 饮水量输入验证,防止非数字和负数
- 目标调整边界处理,限制在合理范围内
- 只显示今日的记录,历史记录不显示在今日列表中
十一、总结
本文基于HarmonyOS和ArkTS实现了一个饮水提醒应用。项目通过@State管理页面数据,使用ArkUI组件完成页面布局,实现了饮水记录、进度显示、目标设置和记录管理等功能。这个项目展示了ArkTS中圆形进度、快捷交互和数据统计的基本方法,适合作为入门练习项目。
更多推荐



所有评论(0)