应用定位:健康习惯模拟类小应用
技术栈:HarmonyOS NEXT + ArkTS + Stage模型
源码行数:108行
核心亮点:目标系统、组件复用、视觉反馈


一、引言

"多喝水"是每个人都听过无数遍的健康建议,但真正做到每天喝够2000ml的人却不多。喝水模拟器的创意正是将这一健康目标游戏化——通过模拟饮水过程,让用户在轻松的点击中获得达成目标的成就感。

从技术角度看,喝水模拟器引入了上一节吃饭模拟器没有的几个新课题:

  • 目标驱动系统:2000ml 目标,进度条直观反馈
  • 多档位操作:小杯200ml / 大杯500ml,不同操作产生不同增量
  • 容器切换:用数组索引切换不同杯型 emoji
  • 负向操作:"去厕所"减少体内水量,模拟真实生理反馈
  • 达标检测:达到目标时触发庆祝状态

这些看似简单的功能,背后蕴含了ArkTS声明星式UI中关于状态粒度设计、数值安全操作、目标达成判定等重要概念。


二、需求分析

2.1 功能需求

需求ID 功能名称 描述
F1 饮水记录 记录今日已喝ml数和杯数
F2 目标展示 显示2000ml目标进度
F3 小杯饮水 点击喝200ml
F4 大杯饮水 点击喝500ml
F5 杯型切换 在🥤🧃🍵🥛🧊五种杯型间切换
F6 排水操作 "去厕所"减少体内水量
F7 达标庆祝 达到2000ml时显示庆祝信息
F8 重置 新的一天重置所有数据

2.2 状态模型

┌─────────────────────────────────┐
│        喝水模拟器状态树            │
├─────────────────────────────────┤
│ totalDrink: number   // 总饮水量   │
│ cups: number         // 杯数      │
│ waterLevel: number   // 水量(备用)  │
│ message: string      // 提示消息    │
│ cupIndex: number     // 杯型索引    │
└─────────────────────────────────┘

三、核心代码逐层解析

3.1 组件入口与状态定义

@Entry
@Component
struct Index2 {
  @State totalDrink: number = 0;
  @State cups: number = 0;
  @State waterLevel: number = 0;
  @State message: string = '多喝水有益健康!💧';
  @State cupIndex: number = 0;
  private cupIcons: string[] = ['🥤', '🧃', '🍵', '🥛', '🧊'];

设计观察waterLevel 在当前版本中未被UI直接使用,属于"预留状态"。在实际开发中,这属于应该被清理的死代码——但在教学示例中,它展示了 @State 可以声明但暂不使用的灵活性。

private cupIcons 未用 @State 标记
这是一个重要的设计决策——cupIcons 是常量数组,在组件的整个生命周期中不会改变。因此不需要用 @State 装饰,避免了不必要的状态监听开销。

3.2 UI视图层

build() {
  Column() {
    Text('💧 喝水模拟器')
      .fontSize(24)
      .fontWeight(FontWeight.Bold)
      .margin({ top: 20, bottom: 5 })

    Text(this.message)
      .fontSize(14)
      .fontColor('#666')
      .margin({ bottom: 15 })

    Text(this.cupIcons[this.cupIndex])
      .fontSize(72)
      .margin(10)

    Text('今日已喝 ' + this.cups + ' 杯 (' + this.totalDrink + 'ml)')
      .fontSize(16)
      .margin(5)

    Progress({ value: this.totalDrink, total: 2000, type: ProgressType.Linear })
      .width('80%')
      .color('#2196F3')
      .margin({ bottom: 5 })
    Text('目标 2000ml')
      .fontSize(12)
      .fontColor('#999')
      .margin({ bottom: 20 })

    // ... 按钮区域
  }
}
Progress组件深入

这里的 Progress 与吃饭模拟器不同:

Progress({ value: this.totalDrink, total: 2000, type: ProgressType.Linear })
  • total 显式设为 2000,而不是默认的 100
  • 这意味着进度条会在 value === 2000 时达到 100%

这种"自定义总量"的 Progress 非常适合目标达成类场景——目标值不是固定的100%,而是根据业务逻辑设定(这里是2000ml)。

3.3 核心交互逻辑

饮水操作
Button('🥤 小杯 (200ml)')
  .onClick(() => {
    this.drink(200);
  })
  .backgroundColor('#2196F3')
  .margin({ right: 5 })

Button('🍵 大杯 (500ml)')
  .onClick(() => {
    this.drink(500);
  })
  .backgroundColor('#1976D2')

这里采用了方法抽取设计模式——将通用的"喝水"逻辑提取到 drink(ml) 方法中:

drink(ml: number) {
  this.totalDrink += ml;
  this.cups++;
  this.message = '咕噜咕噜... 喝了 ' + ml + 'ml!😊';
  if (this.totalDrink >= 2000) {
    this.message = '🎉 达标啦!今天喝了 ' + this.totalDrink + 'ml!';
  }
}

为什么抽取为方法?

  1. 避免代码重复:两个按钮调用同一逻辑,只是参数不同
  2. 逻辑内聚:饮水计算的业务逻辑集中在一处
  3. 便于测试:方法可独立测试,无需模拟UI交互
  4. 便于扩展:如果要加"超大杯1000ml",只需添加 this.drink(1000)

达标判定

if (this.totalDrink >= 2000) {
  this.message = '🎉 达标啦!今天喝了 ' + this.totalDrink + 'ml!';
}

注意这里使用 >= 而不是 ===——因为大杯500ml可能从1600ml直接跳到2100ml,用 === 2000 会漏判。

杯型切换
Button('🧊 加冰')
  .onClick(() => {
    this.cupIndex = (this.cupIndex + 1) % this.cupIcons.length;
    this.message = '换了一种杯子!' + this.cupIcons[this.cupIndex];
  })

循环取模技巧(this.cupIndex + 1) % this.cupIcons.length 是一种经典的循环下标递增写法:

  • cupIndex 为 0~3 时,+1 后正常递增
  • cupIndex 为 4(最后一个元素)时,(4+1) % 5 = 0,回到开头

这避免了手动判断边界的 if-else。

排水操作
Button('🚽 去厕所')
  .onClick(() => {
    if (this.totalDrink < 100) {
      this.message = '才喝这么点,不用去厕所吧😅';
      return;
    }
    this.totalDrink = Math.max(0, this.totalDrink - 500);
    this.message = '排空一下,轻松多了!💪';
  })
  .backgroundColor('#9C27B0')

这个操作模拟了真实的生理反馈——喝完水后需要排水。设计要点:

  1. 守卫条件totalDrink < 100 时拦截,给出幽默提示
  2. 数值下限保护Math.max(0, ...) 防止负值
  3. 固定减量:每次减少500ml,模拟一次"厕所"

四、ArkTS数值操作安全模式

在 ArkTS 中,对于数值类型的状态更新,有几种安全模式值得总结:

4.1 上限保护模式

value = Math.min(max, value + increment);

适用于:进度、等级、分数等有上限的数值

4.2 下限保护模式

value = Math.max(min, value - decrement);

适用于:血量、体力、余额等不能为负的数值

4.3 循环回绕模式

index = (index + step) % length;

适用于:轮播图、选项切换、颜色循环等

4.4 安全增减函数封装

clamp(value: number, min: number, max: number): number {
  return Math.min(max, Math.max(min, value));
}

addSafely(current: number, delta: number, max: number): number {
  return this.clamp(current + delta, 0, max);
}

将这些工具方法提取到一个公共模块中,可以在多个模拟器间复用。


五、条件渲染与进阶UI

5.1 达标庆祝

if (this.totalDrink >= 2000) {
  Text('🎉 今日饮水目标达成!太棒了!')
    .fontSize(16)
    .fontColor('#4CAF50')
    .fontWeight(FontWeight.Bold)
    .margin({ top: 20 })
}

这个条件渲染在目标达成时展示祝贺信息,使用了:

  • 绿色 #4CAF50 增强正面反馈
  • FontWeight.Bold 加粗强调
  • 上方 margin 与其他元素保持间距

5.2 颜色语义化

本应用中的按钮颜色设计遵循了语义化原则:

按钮 颜色 语义
小杯 #2196F3 蓝色 基础操作
大杯 #1976D2 深蓝 重要操作
加冰 #00BCD4 青色 辅助操作
去厕所 #9C27B0 紫色 负向操作
重置 #888 灰色 危险操作

这种颜色编码使用户一眼就能区分不同操作的性质,无需阅读文字说明。


六、鸿蒙的progress组件详解

Progress 是 HarmonyOS 提供的一个非常实用的组件,除了本应用使用的线性进度条外,还有多种形态:

6.1 ProgressType枚举

枚举值 描述 适用场景
ProgressType.Linear 水平线性条 目标进度、加载进度
ProgressType.Circular 圆形进度环 加载等待、完成度
ProgressType.Eclipse 椭圆形 自定义形态
ProgressType.Scale 刻度式 仪表盘样式
ProgressType.Moon 月牙形 特殊视觉效果

6.2 Progress样式配置

Progress({ value: 50, total: 100, type: ProgressType.Linear })
  .width('80%')          // 宽度
  .height(20)            // 高度(线性条厚度)
  .color('#2196F3')      // 前景色
  .backgroundColor('#E3F2FD')  // 背景色
  .borderRadius(10)      // 圆角
  .animation({ duration: 500 }) // 动画

在喝水模拟器中,我们利用 Progress 的自定义 total 参数,将业务目标(2000ml)直接映射为进度条最大值,实现了业务逻辑与UI的无缝对接。


七、代码复杂度分析

7.1 圈复杂度

本应用的核心逻辑 drink() 方法只有 7 行代码,分支数为 2(if-else),圈复杂度为 2。这是非常理想的指标——代码简单、易于理解和测试。

7.2 耦合度

所有状态都集中在单个组件内,UI与业务逻辑没有分离。对于不足100行的小应用,这种高内聚的设计是合理的。如果应用扩展到500行以上,应该考虑:

  1. 提取 ViewModel:将业务逻辑从组件中分离
  2. 使用 @Prop 和 @Link:将UI拆分为多个子组件
  3. 引入状态管理模式:使用 AppStorage 或 LocalStorage

八、与吃饭模拟器的对比

对比维度 吃饭模拟器 喝水模拟器 技术差异
目标系统 无明确目标 2000ml目标 目标驱动vs自由模式
操作档位 单档位 小杯/大杯双档位 参数化方法
负向操作 消化(减饱腹) 厕所(减水量) 类似模式
条件渲染 1处 1处
方法抽取 无(逻辑内联) drink()方法 代码组织改进
常量管理 食物列表在内部 杯型列表在内部 一致

九、扩展思考

9.1 数据持久化

当前版本每次打开应用都会从0开始。实际场景中,应该将每日饮水量本地持久化:

import { Preferences } from '@kit.ArkData';

// 保存
Preferences.save('today_water', this.totalDrink);

// 读取
this.totalDrink = Preferences.get('today_water', 0);

9.2 多日统计

可以增加日历视图,显示过去7天或30天的饮水记录。这涉及到:

  1. 使用 @State 存储历史数据
  2. 使用 ForEach 渲染历史列表
  3. 使用图表展示趋势

9.3 定时提醒

结合鸿蒙的 BackgroundTaskManager,可以在后台定时发送通知提醒喝水:

import { notificationManager } from '@kit.NotificationKit';

9.4 个性化设置

  • 可自定义目标饮水量(1500ml / 2000ml / 2500ml)
  • 可自定义杯型大小
  • 可自定义提醒间隔

十、总结

喝水模拟器在吃饭模拟器的基础上,引入了几个重要的ArkTS开发概念:

  1. 参数化方法drink(ml) 实现了代码复用
  2. 自定义Progress总量:让进度条直接反映业务目标
  3. 循环模运算:简洁的UI切换逻辑
  4. 按钮颜色语义化:提升用户体验的细节

作为20个模拟器系列的第二篇,我们看到ArkTS在构建带有"目标系统"和"多档位操作"的应用时,代码依然保持在100行左右的轻量级,体现了ArkTS在小型应用开发中的高效性。


Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐