Flutter三方库适配OpenHarmony【pomodoro_timer】番茄钟计时器项目完整实战
Flutter三方库适配OpenHarmony【pomodoro_timer】番茄钟计时器项目完整实战
前言
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
pomodoro_timer 是一个典型的 Flutter 效率工具项目。它围绕番茄工作法实现了 Work、Short Break、Long Break 三种模式,默认工作 25 分钟、短休息 5 分钟、长休息 15 分钟,并提供 Start、Pause、Reset、完成弹窗、番茄数统计和专注时长展示等基础能力。
番茄钟应用看起来只是一个倒计时页面,但它实际覆盖了 状态机设计、异步循环、倒计时刷新、模式切换、弹窗提示、统计展示、动态配色、Material 图标和 OpenHarmony 基础交互适配。对于 Flutter 跨端迁移来说,这类计时器项目很适合验证 UI、异步任务和生命周期边界。

图示说明:本文围绕 Flutter 工程中的 pomodoro_timer 项目展开,重点分析三模式番茄钟的状态设计、倒计时循环、完成弹窗、统计面板、真实工程边界和 OpenHarmony 适配要点。
番茄钟的核心不是显示一个时间数字,而是让“工作、暂停、完成、休息、再开始”这一套状态流转稳定可靠。
本文将基于项目真实源码展开,重点包括:
PomodoroTimerApp的应用入口与 Material 3 主题_selectedMode如何控制 Work、Short Break、Long Break_modeDurations如何定义三种模式时长_remainingSeconds如何驱动倒计时展示Future.doWhile与Future.delayed如何实现秒级循环_completedPomodoros如何统计完成的番茄数- 完成弹窗如何引导进入短休息或长休息
_totalMinutes当前统计逻辑的真实问题- OpenHarmony 适配时需要重点验证哪些 UI 和异步行为
一、项目背景与目标
1.1 项目定位
pomodoro_timer 是一个轻量番茄钟工具,适合用来练习 Flutter 中的局部状态、计时逻辑和工具类页面设计。它没有接入通知、后台保活、音频提醒或本地持久化,主要聚焦前台页面内的倒计时体验。
从用户视角看,应用流程是:
- 选择 Work、Short Break 或 Long Break 模式。
- 点击
Start开始倒计时。 - 运行中点击
Pause暂停。 - 点击
Reset重置当前模式时间。 - Work 模式结束后弹出完成提示。
- 根据完成次数选择短休息或长休息。
从工程视角看,它对应的是:
- 使用
_selectedMode表示当前模式。 - 使用
_remainingSeconds表示剩余秒数。 - 使用
_isRunning表示计时器是否运行。 - 使用异步循环每秒更新状态。
- 使用
_completedPomodoros记录完成番茄数。 - 使用
showDialog展示工作完成提示。 - 使用颜色列表让界面随模式变化。
1.2 当前功能概览
| 功能 | 当前实现 | 技术点 |
|---|---|---|
| 应用入口 | runApp(const PomodoroTimerApp()) |
Flutter 启动流程 |
| 应用主题 | 红色 Material 3 | ColorScheme.fromSeed |
| 三种模式 | Work / Short Break / Long Break | _selectedMode |
| 模式时长 | 25 / 5 / 15 分钟 | _modeDurations |
| 倒计时 | 每秒递减 | Future.doWhile |
| 开始暂停 | Start / Pause 切换 | _isRunning |
| 重置 | 恢复当前模式初始时长 | _updateRemainingTime() |
| 完成弹窗 | Work 结束后显示 | AlertDialog |
| 统计 | 完成番茄数、专注时间 | 底部统计面板 |
| 动态配色 | 模式色变化 | 红、绿、蓝 |
1.3 适合学习的能力
这个项目适合学习:
- 状态机式 UI 设计
- 异步循环倒计时
- 秒数到 mm:ss 的格式化
- 模式切换与状态重置
- AlertDialog 完成提示
- Material 图标与按钮组合
- OpenHarmony 前台计时体验验证
二、环境准备与工程结构
2.1 技术栈概览
项目依赖非常轻,主要使用 Flutter SDK 自身能力。
| 类别 | 当前使用 | 说明 |
|---|---|---|
| 开发语言 | Dart | Flutter 应用主语言 |
| UI 框架 | Flutter Material | 页面、按钮、弹窗、图标 |
| 状态管理 | StatefulWidget + setState |
页面内状态 |
| 计时机制 | Future.doWhile + Future.delayed |
秒级倒计时 |
| 数据配置 | List<int> |
三种模式分钟数 |
| 弹窗 | showDialog + AlertDialog |
完成提醒 |
| 目标适配 | Flutter / OpenHarmony | 基础 UI 与异步行为验证 |
2.2 pubspec 关键配置
工程配置如下:
environment:
sdk: ^3.9.2
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.8
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^5.0.0
flutter:
uses-material-design: true
这里需要关注两点:
- 项目没有引入计时器、通知或持久化三方插件。
uses-material-design: true用于保证 Material Icons 字体资源可用。
2.3 主源码结构
核心代码集中在 lib/main.dart:
import 'package:flutter/material.dart';
void main() {
runApp(const PomodoroTimerApp());
}
主要结构如下:
| 结构 | 类型 | 作用 |
|---|---|---|
PomodoroTimerApp |
StatelessWidget |
应用根组件 |
PomodoroTimerHomePage |
StatefulWidget |
番茄钟首页 |
_PomodoroTimerHomePageState |
State |
管理模式、倒计时和统计 |
2.4 常用运行命令
完成 Flutter 环境准备后,可以执行:
flutter pub get
flutter analyze
flutter test
flutter run
如果在 OpenHarmony 环境运行,需要结合本地 Flutter OpenHarmony 发行版、DevEco Studio、设备连接和签名配置执行对应构建命令。
三、应用入口与主题配置
3.1 main 函数
应用入口非常标准:
void main() {
runApp(const PomodoroTimerApp());
}
runApp 会挂载根组件,后续所有页面都从 PomodoroTimerApp 开始构建。
3.2 PomodoroTimerApp 根组件
根组件代码如下:
class PomodoroTimerApp extends StatelessWidget {
const PomodoroTimerApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
title: 'Pomodoro Timer',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.red),
useMaterial3: true,
),
home: const PomodoroTimerHomePage(title: 'Pomodoro Timer'),
);
}
}
它完成了三件事:
- 设置应用标题为
Pomodoro Timer。 - 使用红色作为 Material 3 种子色。
- 将首页设置为
PomodoroTimerHomePage。
3.3 红色主题的意义
番茄钟常用红色代表工作模式,因此根主题使用:
ColorScheme.fromSeed(seedColor: Colors.red)
不过页面实际 AppBar 和计时器颜色会跟随当前模式变化:
- Work:红色
- Short Break:绿色
- Long Break:蓝色
这让用户不用只看文字,也能通过颜色判断当前状态。
四、页面状态设计
4.1 状态字段
页面状态集中在 _PomodoroTimerHomePageState:
class _PomodoroTimerHomePageState extends State<PomodoroTimerHomePage> {
int _selectedMode = 0;
int _remainingSeconds = 25 * 60;
bool _isRunning = false;
int _completedPomodoros = 0;
int _totalMinutes = 0;
final List<int> _modeDurations = [25, 5, 15];
}
字段含义如下:
| 字段 | 类型 | 作用 |
|---|---|---|
_selectedMode |
int |
当前模式,0 工作、1 短休息、2 长休息 |
_remainingSeconds |
int |
当前剩余秒数 |
_isRunning |
bool |
倒计时是否运行 |
_completedPomodoros |
int |
已完成工作番茄数 |
_totalMinutes |
int |
当前源码中用于统计专注时长 |
_modeDurations |
List<int> |
三种模式的分钟配置 |
4.2 模式索引设计
源码用整数表示模式:
int _selectedMode = 0; // 0 = Work, 1 = Short Break, 2 = Long Break
这种方式很轻量,但可读性依赖注释和数组顺序。当前项目中有三组数组都依赖这个索引:
final List<int> _modeDurations = [25, 5, 15];
final modeColors = [Colors.red, Colors.green, Colors.blue];
final modeLabels = ['Work', 'Short Break', 'Long Break'];
只要三个列表顺序保持一致,页面就能正确工作。
4.3 可读性增强方向
如果项目继续扩展,可以把模式抽象成枚举:
enum PomodoroMode {
work,
shortBreak,
longBreak,
}
再用模型保存标签、颜色和时长:
class PomodoroModeConfig {
const PomodoroModeConfig({
required this.label,
required this.minutes,
required this.color,
});
final String label;
final int minutes;
final Color color;
}
这样能减少多个列表靠索引对齐带来的维护风险。
五、模式配置与时间初始化
5.1 模式时长配置
三种模式时长保存在 _modeDurations:
final List<int> _modeDurations = [25, 5, 15]; // minutes
对应关系如下:
| 索引 | 模式 | 时长 |
|---|---|---|
| 0 | Work | 25 分钟 |
| 1 | Short Break | 5 分钟 |
| 2 | Long Break | 15 分钟 |
5.2 initState 初始化
页面初始化时会调用 _updateRemainingTime():
void initState() {
super.initState();
_updateRemainingTime();
}
该方法会根据当前模式设置剩余秒数。
5.3 更新剩余时间
时间重置逻辑如下:
void _updateRemainingTime() {
setState(() {
_remainingSeconds = _modeDurations[_selectedMode] * 60;
});
}
它将分钟数转换成秒:
_modeDurations[_selectedMode] * 60
例如 Work 模式为:
25 * 60 = 1500 秒
5.4 嵌套 setState 的边界
当前 _resetTimer() 和 _selectMode() 内部会在 setState 中调用 _updateRemainingTime(),而 _updateRemainingTime() 自己也调用 setState:
void _resetTimer() {
setState(() {
_isRunning = false;
_updateRemainingTime();
});
}
这种嵌套写法不够清晰。更稳妥的方式是让 _updateRemainingTime() 只负责计算,不直接调用 setState,或者在外层一次性更新所有字段。
六、倒计时启动逻辑
6.1 _startTimer 方法
启动倒计时的方法如下:
void _startTimer() {
setState(() {
_isRunning = true;
});
Future.doWhile(() async {
await Future.delayed(const Duration(seconds: 1));
if (!mounted || !_isRunning) return false;
setState(() {
_remainingSeconds--;
_totalMinutes++;
});
if (_remainingSeconds <= 0) {
_isRunning = false;
if (_selectedMode == 0) {
_completedPomodoros++;
_showCompletedDialog();
}
return false;
}
return true;
});
}
这个方法的核心是:
- 先把
_isRunning设置为true。 - 进入
Future.doWhile异步循环。 - 每次等待 1 秒。
- 如果页面已卸载或暂停,则停止循环。
- 每秒减少剩余时间。
- 时间结束后停止,并在 Work 模式下弹出完成提示。
6.2 Future.doWhile 的作用
Future.doWhile 会反复执行一个异步函数,只要返回 true 就继续循环:
Future.doWhile(() async {
await Future.delayed(const Duration(seconds: 1));
return true;
});
在这个项目中,它模拟了一个前台计时器。
6.3 mounted 判断
循环中有这段保护:
if (!mounted || !_isRunning) return false;
mounted 可以避免页面销毁后继续调用 setState。这是异步 UI 代码中很重要的防护。
6.4 当前计时方式的边界
当前实现没有使用 Timer.periodic,也没有在 dispose 中取消计时器对象。它依赖 _isRunning 和 mounted 让异步循环停止。
这种方案适合前台轻量演示,但不适合严格计时场景。真实生产级番茄钟通常需要考虑:
- 应用进入后台后的时间流逝。
- 系统暂停任务后的补偿。
- 通知提醒。
- 屏幕熄灭后恢复。
- 以真实时间戳计算剩余时间。
七、暂停与重置逻辑
7.1 暂停计时器
暂停方法如下:
void _pauseTimer() {
setState(() {
_isRunning = false;
});
}
当 _isRunning 变成 false 后,Future.doWhile 下一次循环会返回 false,倒计时停止。
7.2 重置计时器
重置方法如下:
void _resetTimer() {
setState(() {
_isRunning = false;
_updateRemainingTime();
});
}
它的目标是停止运行,并把剩余时间恢复为当前模式的初始时长。
7.3 更清晰的重置写法
为了避免嵌套 setState,可以写成:
void _resetTimer() {
setState(() {
_isRunning = false;
_remainingSeconds = _modeDurations[_selectedMode] * 60;
});
}
这样一次状态更新就能完成所有字段修改。
7.4 按钮状态切换
页面根据 _isRunning 显示 Start 或 Pause:
if (!_isRunning)
ElevatedButton.icon(
onPressed: _startTimer,
icon: const Icon(Icons.play_arrow),
label: const Text('Start'),
)
else
ElevatedButton.icon(
onPressed: _pauseTimer,
icon: const Icon(Icons.pause),
label: const Text('Pause'),
)
这是一种非常直观的状态驱动 UI。
八、模式切换逻辑
8.1 _selectMode 方法
模式切换方法如下:
void _selectMode(int mode) {
setState(() {
_selectedMode = mode;
_isRunning = false;
_updateRemainingTime();
});
}
它会:
- 更新当前模式。
- 停止正在运行的计时。
- 按新模式重置剩余时间。
8.2 顶部模式按钮
顶部使用 List.generate 生成三个模式按钮:
children: List.generate(3, (index) {
final isSelected = _selectedMode == index;
return GestureDetector(
onTap: () => _selectMode(index),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: isSelected ? modeColors[index] : Colors.grey.shade200,
borderRadius: BorderRadius.circular(20),
),
child: Text(modeLabels[index]),
),
);
})
被选中的模式会使用对应颜色,未选中的模式使用浅灰色。
8.3 模式颜色和标签
build 方法中定义了颜色与标签:
final modeColors = [Colors.red, Colors.green, Colors.blue];
final modeLabels = ['Work', 'Short Break', 'Long Break'];
对应关系如下:
| 模式 | 颜色 | 标签 |
|---|---|---|
| Work | 红色 | Work |
| Short Break | 绿色 | Short Break |
| Long Break | 蓝色 | Long Break |
8.4 模式切换的体验边界
当前点击任意模式都会停止计时并重置到该模式初始时间。这种行为适合简单工具,但对已经进入专注状态的用户来说,可能需要二次确认。
如果后续扩展,可以在运行中切换模式时弹出确认对话框,避免误触导致当前计时丢失。
九、时间格式化与显示
9.1 _formatTime 方法
时间格式化方法如下:
String _formatTime() {
final minutes = (_remainingSeconds ~/ 60).toString().padLeft(2, '0');
final seconds = (_remainingSeconds % 60).toString().padLeft(2, '0');
return '$minutes:$seconds';
}
它把秒数转换成 mm:ss 格式。
9.2 padLeft 的作用
padLeft(2, '0') 用于补齐两位数:
| 原始值 | 格式化 |
|---|---|
| 5 | 05 |
| 9 | 09 |
| 12 | 12 |
因此 25 分钟会显示为:
25:00
9.3 圆形计时器 UI
倒计时显示在一个圆形容器中:
Container(
width: 250,
height: 250,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: modeColors[_selectedMode],
width: 8,
),
),
child: Center(
child: Text(
_formatTime(),
style: TextStyle(
fontSize: 56,
fontWeight: FontWeight.bold,
fontFamily: 'monospace',
color: modeColors[_selectedMode],
),
),
),
)
圆形边框颜色跟随模式变化,时间数字使用等宽字体,便于倒计时过程中保持视觉稳定。
十、完成弹窗与休息选择
10.1 完成条件
当剩余时间小于等于 0 时,源码会停止计时:
if (_remainingSeconds <= 0) {
_isRunning = false;
if (_selectedMode == 0) {
_completedPomodoros++;
_showCompletedDialog();
}
return false;
}
只有 Work 模式结束时才会增加番茄数并展示完成弹窗。
10.2 完成弹窗结构
弹窗代码如下:
void _showCompletedDialog() {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Pomodoro Complete!'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.celebration, size: 64, color: Colors.amber),
const SizedBox(height: 16),
const Text('Great job! Take a break.'),
const SizedBox(height: 8),
Text('You\\'ve completed $_completedPomodoros pomodoros today!'),
],
),
),
);
}
弹窗展示完成提示、庆祝图标和今日完成数量。
10.3 Take Break 逻辑
弹窗中的 Take Break 按钮会根据完成番茄数选择休息类型:
_selectMode(_completedPomodoros % 4 == 0 ? 2 : 1);
逻辑含义是:
| 完成番茄数 | 选择模式 |
|---|---|
| 1 | Short Break |
| 2 | Short Break |
| 3 | Short Break |
| 4 | Long Break |
| 5 | Short Break |
| 8 | Long Break |
这符合常见番茄工作法中“每 4 个番茄进入长休息”的节奏。
10.4 Start New 逻辑
Start New 按钮会回到 Work 模式:
Navigator.pop(context);
_selectMode(0);
这表示用户可以跳过休息,直接开始下一轮工作。
十一、底部统计面板
11.1 统计区域结构
页面底部使用 Container 展示统计:
Container(
padding: const EdgeInsets.all(24),
color: Colors.grey.shade100,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
// Completed
// Focus Time
],
),
)
它包含两个统计项:
- Completed:已完成番茄数。
- Focus Time:专注时长。
11.2 完成番茄数
完成番茄数展示如下:
Text(
'$_completedPomodoros',
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
)
只有 Work 模式完整结束时,_completedPomodoros 才会增加。
11.3 专注时长当前问题
源码里每秒执行:
_totalMinutes++;
但底部展示写成:
'${_totalMinutes ~/ 60}h ${_totalMinutes % 60}m'
这里存在真实的统计问题:_totalMinutes 实际上每秒加 1,更像是总秒数,但展示时把它当成分钟来格式化。这样运行 60 秒会显示 1h 0m,而不是 0h 1m。
11.4 更合理的统计写法
可以把字段改名为 _totalSeconds:
int _totalSeconds = 0;
每秒递增:
_totalSeconds++;
展示时转换成小时和分钟:
final hours = _totalSeconds ~/ 3600;
final minutes = (_totalSeconds % 3600) ~/ 60;
最终展示:
Text('${hours}h ${minutes}m')
这样才符合真实时间含义。
十二、OpenHarmony 适配要点
12.1 适配关注范围
pomodoro_timer 没有使用后台通知、系统闹钟、音频提醒或持久化插件,因此适配重点集中在 Flutter 前台能力。
| 适配项 | 涉及源码 | 验证重点 |
|---|---|---|
| MaterialApp | 根组件 | 应用启动和主题 |
| AppBar | Scaffold.appBar |
动态背景色 |
| GestureDetector | 模式按钮 | 点击事件 |
| Container 圆形 | 倒计时区域 | 圆形边框渲染 |
| Text | 时间和统计 | 等宽字体、字号 |
| ElevatedButton | Start/Pause/Reset | 状态切换 |
| AlertDialog | 完成提示 | 弹窗显示和关闭 |
| Future.doWhile | 倒计时循环 | 秒级刷新 |
| Material Icons | 多个图标 | 字体资源加载 |
12.2 图标资源验证
项目使用了以下 Material Icons:
Icons.play_arrow
Icons.pause
Icons.refresh
Icons.celebration
Icons.check_circle
Icons.timer
OpenHarmony 设备上需要确认:
- 图标是否正常显示。
- 图标颜色是否符合预期。
- 图标与按钮文字是否对齐。
- Material Icons 字体是否正确打入包体。
12.3 倒计时刷新验证
倒计时依赖异步循环:
await Future.delayed(const Duration(seconds: 1));
需要观察:
- Start 后是否每秒递减。
- Pause 后是否停止递减。
- Reset 后是否恢复当前模式初始时间。
- 切换模式后是否停止旧计时。
- Work 结束后是否弹出完成提示。
12.4 前后台边界
当前项目没有处理后台计时。如果应用进入后台后系统暂停 Dart 执行,恢复前台时倒计时可能不会按真实时间补偿。
更严谨的生产方案通常会记录开始时间:
DateTime? _startedAt;
恢复时用当前时间差计算剩余秒数,而不是完全依赖每秒循环。
十三、测试与验证
13.1 静态分析
建议先执行:
flutter analyze
重点关注:
- 嵌套
setState写法。 - 统计字段命名与实际含义。
- 异步回调中使用
context的时机。 - 计时状态切换后是否可能重复启动。
13.2 组件测试方向
可以执行:
flutter test
适合覆盖的行为包括:
- 初始显示
25:00。 - 点击 Short Break 后显示
05:00。 - 点击 Long Break 后显示
15:00。 - Start 后按钮切换为 Pause。
- Pause 后按钮切换回 Start。
- Reset 后时间恢复当前模式初始值。
13.3 示例测试代码
下面是一段测试模式切换的思路:
testWidgets('switches to short break', (tester) async {
await tester.pumpWidget(const PomodoroTimerApp());
expect(find.text('25:00'), findsOneWidget);
await tester.tap(find.text('Short Break'));
await tester.pumpAndSettle();
expect(find.text('05:00'), findsOneWidget);
});
它验证了从 Work 切换到 Short Break 后,剩余时间是否按 5 分钟显示。
13.4 手动验证流程
手动验证可以按如下顺序进行:
- 启动应用,确认默认 Work 模式显示
25:00。 - 点击 Short Break,确认显示
05:00且主题变绿。 - 点击 Long Break,确认显示
15:00且主题变蓝。 - 点击 Start,确认每秒递减。
- 点击 Pause,确认倒计时停止。
- 点击 Reset,确认恢复当前模式初始时间。
- Work 模式倒计时结束后,确认弹出完成提示。
- 点击 Take Break,确认根据番茄数量进入短休息或长休息。
13.5 OpenHarmony 真机观察点
| 场景 | 预期结果 |
|---|---|
| 冷启动 | 默认 Work 模式正常显示 |
| 模式切换 | 颜色和时间同步变化 |
| Start | 时间每秒递减 |
| Pause | 时间停止 |
| Reset | 时间恢复初始值 |
| 完成弹窗 | Work 结束后出现 |
| 图标显示 | 所有 Material 图标可见 |
| 小屏幕 | 顶部模式按钮和底部统计不溢出 |
十四、常见问题与优化建议
14.1 为什么专注时长显示不准确
因为 _totalMinutes 每秒递增:
_totalMinutes++;
但展示时按分钟字段处理:
'${_totalMinutes ~/ 60}h ${_totalMinutes % 60}m'
字段语义和展示逻辑不一致。推荐改为 _totalSeconds,再按秒转换成小时和分钟。
14.2 为什么 reset 和 selectMode 不建议嵌套 setState
因为 _updateRemainingTime() 自己已经调用 setState,外层再包一层 setState 会让状态更新逻辑变得绕。
可以改成:
void _selectMode(int mode) {
setState(() {
_selectedMode = mode;
_isRunning = false;
_remainingSeconds = _modeDurations[mode] * 60;
});
}
这样更直接,也更容易维护。
14.3 为什么休息模式结束没有弹窗
源码中只有 Work 模式结束后会弹窗:
if (_selectedMode == 0) {
_completedPomodoros++;
_showCompletedDialog();
}
Short Break 和 Long Break 结束时只会停止计时,不会弹出提示。这是当前实现的真实行为。
14.4 是否适合后台计时
当前实现更适合前台演示,不适合严格后台计时。后台计时通常要结合真实时间戳、生命周期监听、系统通知或平台能力。
例如可以记录目标结束时间:
DateTime? _endAt;
每次刷新时根据当前时间计算:
final remaining = _endAt!.difference(DateTime.now()).inSeconds;
这样比单纯每秒递减更接近真实计时。
14.5 是否需要持久化统计
当前 _completedPomodoros 和专注时长都保存在内存中,应用重启后会清空。如果想记录每日专注数据,需要引入本地存储或后端同步。
十五、工程扩展方向
15.1 使用 Timer.periodic
计时器也可以用 Timer.periodic 实现:
Timer? _timer;
void _startTimer() {
_timer?.cancel();
_timer = Timer.periodic(const Duration(seconds: 1), (_) {
setState(() {
_remainingSeconds--;
});
});
}
使用 Timer 时,需要在 dispose 中取消:
void dispose() {
_timer?.cancel();
super.dispose();
}
15.2 增加自定义时长
可以允许用户设置工作、短休息、长休息时长:
final List<int> _modeDurations = [25, 5, 15];
后续可以通过设置页面修改这三个值,并保存到本地。
15.3 增加声音或震动提醒
番茄钟结束时通常需要提醒用户。可以扩展:
- 声音提示。
- 震动反馈。
- 系统通知。
- 弹窗和通知同时提醒。
这些能力通常会涉及平台插件,OpenHarmony 适配时需要额外验证。
15.4 增加历史统计
可以把完成记录建模为:
class PomodoroRecord {
const PomodoroRecord({
required this.completedAt,
required this.durationMinutes,
});
final DateTime completedAt;
final int durationMinutes;
}
有了记录后,就可以做日统计、周统计和趋势图。
15.5 增加进度环
当前圆形区域只有边框,没有随时间变化的进度。可以使用 CircularProgressIndicator:
CircularProgressIndicator(
value: _remainingSeconds / (_modeDurations[_selectedMode] * 60),
)
这样用户可以直观看到当前周期已经进行到哪里。
总结
pomodoro_timer 用较少代码实现了一个完整的前台番茄钟:它通过 _selectedMode 管理 Work、Short Break、Long Break 三种模式,通过 _modeDurations 保存时长配置,通过 _remainingSeconds 驱动倒计时显示,通过 Future.doWhile 和 Future.delayed 实现秒级循环,通过 _completedPomodoros 统计完成数量,并用 AlertDialog 在工作周期结束后引导用户休息。
从 OpenHarmony 适配角度看,这个项目覆盖了 Material 主题、动态颜色、手势点击、按钮状态切换、圆形 UI、等宽时间文本、异步倒计时、弹窗和 Material Icons 等基础能力,很适合用来验证 Flutter 前台工具类应用在 OpenHarmony 上的表现。
当前源码也有几个真实边界:专注时长统计字段 _totalMinutes 实际按秒递增却按小时和分钟展示,_resetTimer 与 _selectMode 中存在嵌套 setState,休息模式结束没有弹窗提醒,也没有处理后台计时和统计持久化。这些边界不影响项目作为番茄钟入门实战的价值,但在继续工程化时需要优先处理。
如果这篇文章对你有帮助,欢迎点赞、收藏、关注,你的支持是我持续创作的动力!
相关资源:
- OpenHarmony 官网:https://www.openharmony.cn
- OpenHarmony 文档:https://docs.openharmony.cn
- 开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
- Flutter 官网:https://flutter.dev
更多推荐


所有评论(0)