鸿蒙番茄计时器 App 开发实战 — 从零搭建到运行
一、前言
经过计算器和待办事项两个项目的实践,我们已经熟练掌握了 ArkUI 的页面布局、状态管理、列表渲染和事件处理。今天我们将挑战一个更有趣的项目——番茄计时器(Pomodoro Timer)。
番茄工作法是弗朗西斯科·西里洛在 1980 年代创立的时间管理方法,核心规则很简单:专注工作 25 分钟,休息 5 分钟,每完成 4 个循环后进行一次 15-30 分钟的长休息。这种节奏交替工作与休息,既保证了深度专注的效率,又避免了长时间工作的疲劳。
我们将从零开发一个完整的番茄计时器应用,包含倒计时、三种模式切换、自动循环、番茄计数和进度展示。这个项目将带你接触 ArkUI 的定时器 API、生命周期管理、@Builder 组件复用等新的知识点。相比计算器和待办事项,番茄计时器的状态管理更复杂——因为它涉及"正在倒计时"这种随时间变化的状态。
二、项目准备
2.1 开发环境要求
- IDE:DevEco Studio(推荐最新版本)
- SDK:API 23 或以上
- 语言/框架:ArkTS + ArkUI
2.2 新建项目
打开 DevEco Studio,点击 File → New → Create Project,选择 Empty Ability 模板:
| 配置项 | 推荐值 |
|---|---|
| 项目名称 | PomodoroTimer |
| 包名 | com.example.pomodorotimer |
| Compile SDK | API 23 |
| 模块名称 | entry |
点击 Finish 完成创建,等待项目同步完成。
三、功能设计
3.1 界面布局
┌─────────────────────────────┐
│ │
│ 🍅 番茄计时器 │ ← 顶部标题
│ │
│ ┌───────────┐ │
│ │ 25:00 │ │ ← 倒计时数字(大号等宽字体)
│ │ 🍅 专注中 │ │ ← 当前模式标签
│ └───────────┘ │
│ │
│ ┌──────────────────┐ │
│ │ ▶️ 开始 ↺ │ │ ← 控制按钮(开始/暂停 + 重置)
│ └──────────────────┘ │
│ │
│ ┌──────┐ ┌────┐ ┌────┐ │
│ │专注 │ │短休│ │长休│ │ ← 模式切换按钮组
│ └──────┘ └────┘ └────┘ │
│ │
│ 🔁 已完成 3 个番茄 │ ← 底部统计
│ 🍅🍅🍅 │ ← 进度图标
│ │
└─────────────────────────────┘
3.2 三种模式
番茄计时器有三种工作模式,每种有独立的时长和颜色:
| 模式 | 默认时长 | 颜色 | 图标 | 说明 |
|---|---|---|---|---|
| 专注 (focus) | 25 分钟 | 橙色 #FF9F0A | 🍅 | 核心工作时段 |
| 短休 (shortBreak) | 5 分钟 | 绿色 #34C759 | ☕ | 短暂休息 |
| 长休 (longBreak) | 15 分钟 | 蓝色 #5AC8FA | 🌙 | 每 4 个循环后 |
3.3 功能清单
| 功能 | 说明 |
|---|---|
| 倒计时 | 从设定时间逐秒减少,显示 MM:SS 格式 |
| 开始/暂停 | 点击按钮在两种状态间切换 |
| 模式切换 | 手动点击标签切换三种模式 |
| 自动循环 | 专注结束自动切短休,4 次后切长休 |
| 番茄计数 | 每次专注完成计数 +1 |
| 重置 | 停止计时并重置当前模式的剩余时间 |
| 时间到提示 | 显示闪烁的 ⏰ 提示文字 |
| 进度图标 | 每完成一个番茄显示一个 🍅 |
四、核心代码实现
4.1 常量定义
private readonly FOCUS_TIME: number = 25 * 60 // 专注 25 分钟
private readonly SHORT_BREAK: number = 5 * 60 // 短休 5 分钟
private readonly LONG_BREAK: number = 15 * 60 // 长休 15 分钟
private readonly MAX_CYCLES: number = 4 // 4 个循环后长休
所有时间以秒为单位存储。25 × 60 = 1500 秒,5 × 60 = 300 秒,15 × 60 = 900 秒。为什么不用分钟?因为倒计时每秒递减一次,用秒可以直接 -- 操作,不需要做单位转换的额外运算。
4.2 状态变量
@State displayTime: string = '25:00' // 界面显示的倒计时字符串
@State currentMode: string = 'focus' // 当前模式: focus/shortBreak/longBreak
@State isRunning: boolean = false // 是否正在倒计时
@State completedCycles: number = 0 // 已完成的番茄周期数
@State isTimeUp: boolean = false // 时间是否已到
private remainingSeconds: number = 25 * 60 // 剩余秒数(逻辑变量)
private timerId: number = -1 // 定时器 ID,用于销毁
注意 remainingSeconds 和 timerId 没有被 @State 装饰。timerId 是内部引用,不需要驱动 UI;remainingSeconds 每秒变化一次,如果我们直接把它声明为 @State,那每次 setInterval 回调时都会触发变更检测和 UI 重算,性能开销太大。我们的策略是:让 remainingSeconds 在定时器回调中递减,然后手动把格式化后的字符串赋值给 displayTime(唯一 @State),这样每秒只触发一次渲染。
4.3 时间格式化
private formatTime(seconds: number): string {
const mins = Math.floor(seconds / 60)
const secs = seconds % 60
return `${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`
}
padStart(2, '0') 确保分钟和秒始终显示两位数字。比如 5 分 3 秒显示为 05:03 而非 5:3。这在倒计时显示中非常重要——数字跳变时保持位数一致,视觉上更稳定。
4.4 模式配置映射
private getModeTime(mode: string): number {
if (mode === 'focus') return this.FOCUS_TIME
if (mode === 'shortBreak') return this.SHORT_BREAK
return this.LONG_BREAK
}
private getModeLabel(mode: string): string {
if (mode === 'focus') return '🍅 专注中'
if (mode === 'shortBreak') return '☕ 短休中'
return '🌙 长休中'
}
private getModeColor(mode: string): string {
if (mode === 'focus') return '#FF9F0A'
if (mode === 'shortBreak') return '#34C759'
return '#5AC8FA'
}
这三个映射函数将模式标识符(字符串)分别映射为:时长、标签文字、主题色。这样做的好处是:如果需要增加或修改模式,只需在这里改三处,不需要满篇搜索硬编码。
4.5 开始/暂停
private toggleTimer(): void {
if (this.isRunning) {
this.pauseTimer()
} else {
this.startTimer()
}
}
private startTimer(): void {
this.isRunning = true
this.isTimeUp = false
this.timerId = setInterval(() => {
this.remainingSeconds--
if (this.remainingSeconds <= 0) {
this.displayTime = '⏰ 时间到!'
this.onTimeUp()
return
}
this.displayTime = this.formatTime(this.remainingSeconds)
}, 1000)
}
private pauseTimer(): void {
this.isRunning = false
if (this.timerId !== -1) {
clearInterval(this.timerId)
this.timerId = -1
}
}
setInterval(callback, 1000) 创建一个每秒执行一次的定时器。每次回调中:
- 剩余秒数减一
- 如果剩余为 0,显示"时间到"并调用
onTimeUp - 否则更新显示时间
关键细节:setInterval 返回的 timerId 必须在暂停或销毁时调用 clearInterval 清除,否则定时器会持续运行,即使页面关闭或用户切换到了其他 App,定时器依然在后台消耗资源。aboutToDisappear 生命周期就是用来处理这个问题的。
4.6 时间到处理
private onTimeUp(): void {
this.pauseTimer()
this.isTimeUp = true
if (this.currentMode === 'focus') {
this.completedCycles++
if (this.completedCycles % this.MAX_CYCLES === 0) {
this.switchMode('longBreak')
} else {
this.switchMode('shortBreak')
}
} else {
this.switchMode('focus')
}
}
自动循环逻辑:
- 专注结束 → 判断休息类型:如果已完成周期数是 4 的倍数(第 4、8、12…次),自动切换到长休;否则切换到短休
- 休息结束 → 回到专注:无论短休还是长休结束,都自动回到专注模式
这样用户不需要手动操作,计时器会自动在专注和休息之间循环,保持了番茄工作法的完整节奏。
4.7 模式切换
private switchMode(mode: string): void {
if (this.isRunning) this.pauseTimer()
this.currentMode = mode
this.remainingSeconds = this.getModeTime(mode)
this.displayTime = this.formatTime(this.remainingSeconds)
this.isTimeUp = false
}
private selectMode(mode: string): void {
this.switchMode(mode)
this.isRunning = false
}
switchMode 是内部方法,供自动切换和手动切换共用。selectMode 是用户手动点击模式标签时调用的方法,在切换完成后额外确保计时器保持暂停状态——因为用户在切换模式时通常想重新开始计时。
4.8 开始/暂停的防连点保护
倒计时相关的按钮有个容易被忽略的问题:快速连点。用户在紧张或急躁时可能快速多次点击开始按钮,导致多个 setInterval 同时运行,倒计时速度翻倍。
我们的代码通过 isRunning 状态做了天然防护:
private toggleTimer(): void {
if (this.isRunning) {
this.pauseTimer()
} else {
this.startTimer()
}
}
startTimer() 只负责设置定时器,不需要检查 isRunning——因为 toggleTimer 已经保证了在 isRunning = true 时不会调用 startTimer。同理 pauseTimer 在 isRunning = false 时也不会被调用。这个设计模式叫做状态门控(State Gating),用一个布尔变量严格控制两种互斥状态之间的切换。
4.9 重置
private resetTimer(): void {
this.pauseTimer()
this.remainingSeconds = this.getModeTime(this.currentMode)
this.displayTime = this.formatTime(this.remainingSeconds)
this.isTimeUp = false
}
重置将当前模式的时间恢复为初始值,但不切换模式。比如你正在短休模式下倒计时到 2:30,点重置后恢复为 5:00。
4.9 生命周期清理
aboutToDisappear(): void {
this.pauseTimer()
}
当用户离开这个页面时(比如返回上一页或切换到其他 App),调用 aboutToDisappear 确保定时器被清除。这是防止内存泄漏的标准做法。
4.10 进度图标
private getCycleDots(): string[] {
const dots: string[] = []
for (let i = 0; i < this.completedCycles; i++) {
dots.push('🍅')
}
return dots
}
根据已完成周期数生成对应数量的番茄 emoji 数组,用于在底部展示进度。每完成一个专注周期就多显示一个 🍅,成就感满满。
4.11 完整 UI 构建
build() {
Column() {
// ═══ 顶部标题 ═══
Text('🍅 番茄计时器')
.fontSize(28).fontWeight(FontWeight.Bold).fontColor(Color.White)
.padding(20)
// ═══ 倒计时显示区(圆形面板)═══
Column() {
Text(this.displayTime)
.fontSize(72).fontWeight(FontWeight.Bold)
.fontColor(this.isTimeUp ? '#FF3B30' : this.getModeColor(this.currentMode))
.fontFamily('Courier New')
Text(this.getModeLabel(this.currentMode))
.fontSize(18).fontColor(this.getModeColor(this.currentMode))
.margin({ top: 8 })
}
.width(240).height(240).backgroundColor('#1C1C1E')
.borderRadius(120).justifyContent(FlexAlign.Center)
// ═══ 控制按钮 ═══
Row() {
Button(this.isRunning ? '⏸️ 暂停' : '▶️ 开始')
.fontSize(20).fontWeight(FontWeight.Bold)
.width(160).height(56).borderRadius(28)
.backgroundColor(this.getModeColor(this.currentMode))
.fontColor(Color.White)
.onClick(() => { this.toggleTimer() })
Button('↺')
.fontSize(24).width(56).height(56).borderRadius(28)
.backgroundColor('#333333')
.margin({ left: 12 })
.onClick(() => { this.resetTimer() })
}
.margin({ top: 30 })
// ═══ 模式切换标签 ═══
Row() {
this.modeButton('focus', '🍅 专注')
this.modeButton('shortBreak', '☕ 短休')
this.modeButton('longBreak', '🌙 长休')
}
.width('100%').padding({ left: 20, right: 20 })
.margin({ top: 24 })
// ═══ 底部统计 ═══
Text(`🔁 已完成 ${this.completedCycles} 个番茄`)
.fontSize(16).fontColor('#8E8E93')
.margin({ top: 20 })
if (this.completedCycles >= 1) {
Row() {
ForEach(this.getCycleDots(), (dot: string) => {
Text(dot).fontSize(22).margin({ left: 4, right: 4 })
})
}
.margin({ top: 8 })
}
}
.width('100%').height('100%').backgroundColor('#000000')
.justifyContent(FlexAlign.Start)
}
// @Builder 抽离的模式切换按钮
@Builder modeButton(mode: string, label: string) {
Button(label)
.fontSize(14).fontWeight(FontWeight.Medium)
.height(40).borderRadius(20)
.backgroundColor(this.currentMode === mode
? this.getModeColor(mode) : '#2C2C2E')
.fontColor(this.currentMode === mode ? Color.White : '#8E8E93')
.layoutWeight(1)
.margin({ left: 4, right: 4 })
.onClick(() => { this.selectMode(mode) })
}
UI 设计要点:
- 等宽字体:
fontFamily('Courier New')让所有数字宽度一致。普通字体中1比0窄,倒计时数字跳动时会左右晃动;等宽字体消除了这个视觉问题 @Builder:ArkUI 中抽离重复 UI 的利器。modeButton方法创建可复用的标签按钮,通过参数控制颜色和文字。代码中三个标签只写了三行,实际按钮逻辑集中在一处- 条件渲染底部进度:
if (this.completedCycles >= 1)确保在没有完成任何番茄时不显示空行 - 大号圆形倒计时面板:240×240 像素,圆角 120 实现正圆形,视觉上更像一个钟表
五、运行与测试
5.1 替换代码
打开 entry/src/main/ets/pages/Index.ets,全选替换为上面提供的完整代码。
5.2 功能测试
| # | 测试操作 | 预期结果 |
|---|---|---|
| 1 | 启动 App | 显示 25:00,模式标签"🍅 专注"高亮 |
| 2 | 点 ▶️ 开始 | 倒计时开始逐秒递减,按钮变为 ⏸️ 暂停 |
| 3 | 点 ⏸️ 暂停 | 倒计时暂停,按钮恢复 ▶️ |
| 4 | 点 ☕ 短休 | 切换到 05:00,短休标签高亮绿色 |
| 5 | 点 🌙 长休 | 切换到 15:00,长休标签高亮蓝色 |
| 6 | 等待专注结束 | 显示 ⏰ 时间到!,番茄数 +1 |
| 7 | 连续完成 4 个番茄 | 第 4 次自动切换到长休 |
| 8 | 点 ↺ 重置 | 回到当前模式的初始值 |
| 9 | 快速切模式 | 每次切换后时间重置为新模式的值 |
| 10 | 退出再打开 | 番茄数重置为 0(无持久化,建议后续加) |
5.3 调试技巧
测试倒计时时,可以把专注时间临时改成 5 秒以便快速验证:
private readonly FOCUS_TIME: number = 5 // 改成 5 秒方便调试
验证完自动循环逻辑后再改回 25 × 60。
六、运行效果

从这三个项目的变化可以清楚看到鸿蒙开发的学习路径:计算器是静态交互(点一下动一下),待办 App 是数据管理(增删改查),计时器是动态过程(随时间变化)。每一步都比前一步更接近真实 App 的复杂度。
七、扩展思路
- 自定义时长:增加设置页面,用
Slider滑动条让用户自定义专注/休息时长 - 系统通知:时间到时调用
Notification.requestPermission发送系统推送通知 - 每日统计:将每天的完成番茄数用
@StorageLink持久化,用图表展示 - 白噪音:集成简单的白噪音播放(雨声、篝火声),专注时自动播放
- 严格模式:开启后专注期间不可切换模式或暂停,倒逼专注
八、总结
本文从零开始完成了鸿蒙番茄计时器 App 的开发,核心知识点回顾:
- ✅ 定时器:
setInterval/clearInterval实现每秒倒计时 - ✅ 生命周期:
aboutToDisappear清理定时器,防止内存泄漏 - ✅ @Builder:抽离可复用 UI 组件,减少重复代码
- ✅ 状态管理:一个 @State(displayTime)驱动所有 UI 变化
- ✅ 自动循环:专注→短休→专注→…→长休的完整工作流
- ✅ 等宽字体:
fontFamily('Courier New')让数字对齐 - ✅ 条件渲染:
if (completedCycles >= 1)控制进度显示
三个项目循序渐进:计算器训练事件处理和基本布局,待办 App 训练列表和数据持久化,番茄计时器训练定时器和复杂状态管理。完成本系列三篇文章的所有代码,你已经掌握了 ArkUI 开发的核心知识体系,从零搭建一个完整的鸿蒙工具类应用已经不在话下。
更多推荐



所有评论(0)