Flutter 实战:pomodoro_timer 番茄钟的阶段切换、进度环与鸿蒙适配解析
Flutter 实战:pomodoro_timer 番茄钟的阶段切换、进度环与鸿蒙适配解析
前言
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
pomodoro_timer 是一个基于 Flutter 实现的番茄钟应用。它支持工作阶段与休息阶段切换,可以统计已完成番茄数,并提供工作时长、休息时长两个 Slider 配置项。页面通过图标、颜色、倒计时文本和圆形进度环,让用户直观看到当前状态。
本文基于项目真实源码展开,重点分析 番茄钟状态设计、工作/休息阶段切换、Future.doWhile 倒计时循环、完成次数统计、Slider 时长配置 和 鸿蒙适配关注点。文章内容可直接发布到 CSDN,不包含面向作者的检查说明。
番茄钟应用看似只是一个倒计时器,但它比普通计时器多了阶段语义、状态迁移、统计逻辑和配置约束。把这些细节拆开,能很好地理解 Flutter 工具类应用的状态建模方式。

图示说明:本文围绕 Flutter 番茄钟的倒计时循环、阶段切换和跨端渲染链路展开,适合用于鸿蒙、Android、iOS 等平台的小工具应用开发复盘。
一、项目定位与功能概览
1.1 应用主题
pomodoro_timer 的定位是一个 番茄工作法计时工具。用户可以设置工作时长和休息时长,点击 Start 开始当前阶段,点击 Stop 暂停,点击 Skip 跳到下一阶段。
核心功能如下:
| 功能 | 页面表现 | 源码实现 |
|---|---|---|
| 工作阶段 | Work Session |
_isWorkPhase = true |
| 休息阶段 | Break Time |
_isWorkPhase = false |
| 倒计时展示 | 25:00 格式 |
_formattedTime |
| 圆形进度 | 红色或绿色进度环 | CircularProgressIndicator |
| 完成统计 | Completed: x pomodoros |
_completedPomodoros |
| 工作时长设置 | Work Slider | _workDuration |
| 休息时长设置 | Break Slider | _breakDuration |
| 跳过阶段 | Skip 按钮 | _skipPhase() |
1.2 默认行为
源码中的默认配置是:
| 配置项 | 默认值 |
|---|---|
| 工作时长 | 25 分钟 |
| 休息时长 | 5 分钟 |
| 初始阶段 | 工作阶段 |
| 初始剩余时间 | 25 * 60 秒 |
| 完成番茄数 | 0 |
默认进入页面后,用户会看到 Work Session、25:00、Ready 和已完成番茄数统计。
1.3 适合学习的点
这个项目适合学习 Flutter 小工具应用中的以下内容:
- 如何用少量状态字段描述复杂页面状态。
- 如何用异步循环实现倒计时。
- 如何在阶段结束后切换业务状态。
- 如何用 Slider 控制配置项并联动剩余时间。
- 如何面向鸿蒙进行前台计时器适配验证。
二、工程结构与运行方式
2.1 工程结构
项目保持 Flutter 默认工程结构,核心逻辑集中在 lib/main.dart:
| 文件或目录 | 作用 | 说明 |
|---|---|---|
lib/main.dart |
应用入口与页面实现 | 包含番茄钟状态、计时和 UI |
pubspec.yaml |
依赖声明 | 使用 Flutter SDK 与 Material 图标 |
test/widget_test.dart |
Widget 测试入口 | 可扩展为番茄钟业务测试 |
ohos/ |
鸿蒙平台工程目录 | 用于跨端构建和适配 |
2.2 依赖声明
项目没有引入复杂第三方插件:
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.8
这意味着计时逻辑、状态切换和 UI 渲染主要由 Flutter 与 Dart 完成,跨端适配成本相对可控。
2.3 常用命令
开发和验证时可以使用以下命令:
flutter pub get
flutter analyze
flutter test
flutter run
| 命令 | 作用 | 使用场景 |
|---|---|---|
flutter pub get |
获取依赖 | 首次运行或依赖更新 |
flutter analyze |
静态分析 | 检查语法和 lint |
flutter test |
执行测试 | 验证 Widget 行为 |
flutter run |
启动应用 | 本地调试界面 |
三、应用入口与根组件
3.1 main 函数
Flutter 应用入口非常简洁:
void main() {
runApp(const MyApp());
}
番茄钟不需要初始化数据库、网络服务或平台权限,因此入口只负责挂载根组件。
3.2 MyApp 根组件
MyApp 创建 MaterialApp:
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
title: 'Pomodoro Timer',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.red),
),
home: const MyHomePage(title: 'Pomodoro Timer'),
);
}
}
这里包含三个关键信息:
- 应用标题是
Pomodoro Timer。 - 主题种子色是
Colors.red。 - 首页是
MyHomePage。
3.3 主题色与阶段颜色
源码中工作阶段使用红色,休息阶段使用绿色:
| 阶段 | 图标 | 颜色 |
|---|---|---|
| Work Session | Icons.work |
Colors.red |
| Break Time | Icons.coffee |
Colors.green |
这种颜色差异让用户不用仔细阅读文本,也能快速判断当前阶段。
当前源码没有显式设置
useMaterial3: true,因此文章以真实代码为准。若后续希望统一 Material 3 表现,可以在ThemeData中补充该配置。
四、状态字段设计
4.1 核心状态总览
番茄钟的全部核心状态都定义在 _MyHomePageState 中:
int _workDuration = 25;
int _breakDuration = 5;
int _remainingSeconds = 25 * 60;
bool _isRunning = false;
bool _isWorkPhase = true;
int _completedPomodoros = 0;
字段含义如下:
| 字段 | 类型 | 默认值 | 作用 |
|---|---|---|---|
_workDuration |
int |
25 |
工作阶段分钟数 |
_breakDuration |
int |
5 |
休息阶段分钟数 |
_remainingSeconds |
int |
25 * 60 |
当前阶段剩余秒数 |
_isRunning |
bool |
false |
是否运行中 |
_isWorkPhase |
bool |
true |
是否处于工作阶段 |
_completedPomodoros |
int |
0 |
已完成番茄数 |
4.2 状态分层
这些字段可以分为三类:
| 状态类别 | 字段 | 作用 |
|---|---|---|
| 配置状态 | _workDuration、_breakDuration |
控制每个阶段时长 |
| 运行状态 | _remainingSeconds、_isRunning |
控制倒计时过程 |
| 业务状态 | _isWorkPhase、_completedPomodoros |
描述番茄钟语义 |
4.3 为什么需要阶段状态
普通倒计时只关心剩余时间,而番茄钟还要知道当前是工作还是休息。_isWorkPhase 的作用包括:
- 决定页面展示
Work Session还是Break Time。 - 决定图标使用
Icons.work还是Icons.coffee。 - 决定主色使用红色还是绿色。
- 决定当前阶段总时长使用工作时长还是休息时长。
- 决定阶段结束后如何切换下一阶段。
五、Start、Stop 与 Skip 控制逻辑
5.1 开始计时
开始按钮触发 _startTimer():
void _startTimer() {
setState(() {
_isRunning = true;
});
_runTimer();
}
这段代码先把运行状态改为 true,然后启动异步倒计时循环。
5.2 停止计时
停止按钮触发 _stopTimer():
void _stopTimer() {
setState(() {
_isRunning = false;
});
}
停止不会重置剩余时间,因此它更接近“暂停”。用户再次点击 Start,会从当前剩余时间继续。
5.3 跳过阶段
Skip 按钮触发 _skipPhase():
void _skipPhase() {
setState(() {
_isRunning = false;
if (_isWorkPhase) {
_completedPomodoros++;
_isWorkPhase = false;
_remainingSeconds = _breakDuration * 60;
} else {
_isWorkPhase = true;
_remainingSeconds = _workDuration * 60;
}
});
}
这里体现了番茄钟的业务规则:
| 当前阶段 | 点击 Skip 后 | 是否增加完成数 |
|---|---|---|
| 工作阶段 | 切到休息阶段 | 是 |
| 休息阶段 | 切到工作阶段 | 否 |
5.4 重置逻辑
源码中还有 _resetTimer():
void _resetTimer() {
setState(() {
_isRunning = false;
_isWorkPhase = true;
_remainingSeconds = _workDuration * 60;
});
}
当前 UI 没有直接展示 Reset 按钮,但这个方法已经为后续扩展预留了能力。它会停止计时,并回到工作阶段。
六、Future.doWhile 倒计时循环
6.1 核心实现
倒计时循环由 _runTimer() 实现:
void _runTimer() {
Future.doWhile(() async {
if (!_isRunning) return false;
await Future.delayed(const Duration(seconds: 1));
if (!mounted) return false;
setState(() {
if (_remainingSeconds > 0) {
_remainingSeconds--;
} else {
_isRunning = false;
if (_isWorkPhase) {
_completedPomodoros++;
_isWorkPhase = false;
_remainingSeconds = _breakDuration * 60;
} else {
_isWorkPhase = true;
_remainingSeconds = _workDuration * 60;
}
}
});
return _isRunning;
});
}
6.2 循环流程
可以把 _runTimer() 理解成以下流程:
进入 Future.doWhile
-> 如果不是运行中,结束循环
-> 等待 1 秒
-> 如果页面已卸载,结束循环
-> 如果剩余秒数大于 0,递减 1 秒
-> 如果剩余秒数为 0,停止并切换阶段
-> 根据 _isRunning 判断是否继续循环
6.3 mounted 判断的价值
异步延迟之后,页面可能已经被销毁。源码使用 mounted 做保护:
if (!mounted) return false;
这可以避免组件不在树上时继续调用 setState(),是 Flutter 异步 UI 更新中非常重要的安全习惯。
6.4 阶段结束时的状态迁移
当 _remainingSeconds 到 0 后,代码会根据当前阶段切换:
| 当前阶段 | 结束后的动作 |
|---|---|
| 工作阶段 | 完成数加 1,进入休息阶段,剩余时间变为休息时长 |
| 休息阶段 | 进入工作阶段,剩余时间变为工作时长 |
这种状态迁移是番茄钟区别于普通倒计时器的关键。
七、时间格式化与显示
7.1 formattedTime getter
页面中央的时间文本来自 _formattedTime:
String get _formattedTime {
final minutes = _remainingSeconds ~/ 60;
final seconds = _remainingSeconds % 60;
return '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
}
7.2 分钟与秒的计算
代码使用整除和取余:
| 表达式 | 含义 |
|---|---|
_remainingSeconds ~/ 60 |
得到分钟 |
_remainingSeconds % 60 |
得到秒 |
例如 _remainingSeconds = 1499:
final minutes = 1499 ~/ 60; // 24
final seconds = 1499 % 60; // 59
最终显示为 24:59。
7.3 补零显示
padLeft(2, '0') 保证分钟和秒至少两位:
minutes.toString().padLeft(2, '0')
显示效果如下:
| 数值 | 文本 |
|---|---|
5 |
05 |
9 |
09 |
25 |
25 |
7.4 为什么不单独存文本
_formattedTime 是由 _remainingSeconds 派生出来的状态,不需要单独保存。这样可以避免出现“剩余秒数已经变化,但文本没有同步”的问题。
八、圆形进度环实现
8.1 CircularProgressIndicator 配置
页面使用 CircularProgressIndicator 展示当前阶段剩余比例:
CircularProgressIndicator(
value: _remainingSeconds / (_isWorkPhase ? _workDuration * 60 : _breakDuration * 60),
strokeWidth: 12,
backgroundColor: Colors.grey.shade200,
valueColor: AlwaysStoppedAnimation<Color>(
_isWorkPhase ? Colors.red : Colors.green,
),
)
8.2 进度值计算
进度值由剩余秒数除以当前阶段总秒数得到:
progress = remainingSeconds / totalSecondsOfCurrentPhase
| 阶段 | 当前总时长 | 进度分母 |
|---|---|---|
| 工作阶段 | _workDuration 分钟 |
_workDuration * 60 |
| 休息阶段 | _breakDuration 分钟 |
_breakDuration * 60 |
8.3 进度变化示例
以默认工作阶段 25 分钟为例:
| 剩余时间 | 剩余秒数 | 进度值 |
|---|---|---|
| 25:00 | 1500 | 1.0 |
| 12:30 | 750 | 0.5 |
| 05:00 | 300 | 0.2 |
| 00:00 | 0 | 0 |
8.4 Stack 居中叠放
进度环和时间文本通过 Stack 组合:
Stack(
alignment: Alignment.center,
children: [
SizedBox(
width: 200,
height: 200,
child: CircularProgressIndicator(...),
),
Column(
children: [
Text(_formattedTime),
Text(_isRunning ? 'Focus!' : 'Ready'),
],
),
],
)
这种布局适合倒计时、运动进度、健康数据等“中心数值 + 外层进度”的界面。
九、阶段图标与状态文案
9.1 阶段图标
页面顶部根据阶段显示不同图标:
Icon(
_isWorkPhase ? Icons.work : Icons.coffee,
size: 48,
color: _isWorkPhase ? Colors.red : Colors.green,
)
| 阶段 | 图标 | 含义 |
|---|---|---|
| 工作阶段 | Icons.work |
专注工作 |
| 休息阶段 | Icons.coffee |
放松休息 |
9.2 阶段标题
阶段标题同样由 _isWorkPhase 决定:
Text(
_isWorkPhase ? 'Work Session' : 'Break Time',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: _isWorkPhase ? Colors.red : Colors.green,
),
)
9.3 运行状态文案
进度环中心下方显示运行状态:
Text(
_isRunning ? 'Focus!' : 'Ready',
style: TextStyle(color: Colors.grey.shade600),
)
| 状态 | 文案 | 用户理解 |
|---|---|---|
| 未运行 | Ready | 等待开始 |
| 运行中 | Focus! | 正在专注 |
9.4 颜色语义
红色与绿色分别承载工作和休息语义:
- 红色:提醒用户进入专注状态。
- 绿色:表示休息、恢复、放松。
这种简单的颜色语义,对跨端应用尤其重要,因为用户可以在不同设备上快速建立视觉认知。
十、完成番茄数统计
10.1 统计卡片
完成统计区域是一张橙色浅背景卡片:
Card(
color: Colors.orange.shade50,
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.check_circle, color: Colors.orange),
const SizedBox(width: 8),
Text(
'Completed: $_completedPomodoros pomodoros',
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.orange,
),
),
],
),
),
)
10.2 什么时候增加完成数
完成数只在工作阶段结束或跳过工作阶段时增加:
if (_isWorkPhase) {
_completedPomodoros++;
_isWorkPhase = false;
_remainingSeconds = _breakDuration * 60;
}
10.3 为什么休息结束不增加
番茄钟统计的是完成的工作单元,而不是休息单元。因此:
| 事件 | 是否增加完成数 |
|---|---|
| 工作阶段自然结束 | 是 |
| 工作阶段点击 Skip | 是 |
| 休息阶段自然结束 | 否 |
| 休息阶段点击 Skip | 否 |
10.4 统计功能的扩展方向
后续可以把统计扩展为:
- 今日完成番茄数。
- 连续专注次数。
- 历史记录。
- 每日目标。
- 专注时长汇总。
这些功能需要本地存储或云端同步,当前项目暂未涉及。
十一、Slider 时长配置
11.1 工作时长 Slider
工作时长通过 Slider 调整:
Slider(
value: _workDuration * 1.0,
min: 15,
max: 60,
divisions: 9,
label: '$_workDuration min',
onChanged: _isRunning ? null : (val) {
setState(() {
_workDuration = val.round();
if (_isWorkPhase) {
_remainingSeconds = _workDuration * 60;
}
});
},
)
11.2 休息时长 Slider
休息时长配置类似:
Slider(
value: _breakDuration * 1.0,
min: 5,
max: 30,
divisions: 5,
label: '$_breakDuration min',
onChanged: _isRunning ? null : (val) {
setState(() {
_breakDuration = val.round();
if (!_isWorkPhase) {
_remainingSeconds = _breakDuration * 60;
}
});
},
)
11.3 配置范围
| 配置项 | 最小值 | 最大值 | 分段数 |
|---|---|---|---|
| Work | 15 分钟 | 60 分钟 | 9 |
| Break | 5 分钟 | 30 分钟 | 5 |
11.4 运行中禁用配置
Slider 的 onChanged 在运行时为 null:
onChanged: _isRunning ? null : (val) {
// 更新时长
}
这意味着运行中不能调整时长,避免总时长变化后进度环和剩余时间语义变得混乱。
十二、页面布局与视觉层级
12.1 页面整体结构
页面使用 Scaffold 和 SingleChildScrollView:
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Column(
children: [
// 主计时卡片
// 完成统计卡片
// 操作按钮
// 设置卡片
],
),
),
);
SingleChildScrollView 对移动端和鸿蒙设备很重要。页面内容包含计时卡片、统计卡片、按钮和设置卡片,在小屏上需要可滚动。
12.2 主计时卡片
主卡片承载阶段、进度和时间:
Card(
child: Padding(
padding: const EdgeInsets.all(32),
child: Column(
children: [
Icon(...),
const SizedBox(height: 16),
Text(...),
const SizedBox(height: 24),
Stack(...),
],
),
),
)
它是页面视觉中心,用户主要关注这里。
12.3 操作按钮区
操作区包含 Start/Stop 和 Skip:
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton.icon(...),
ElevatedButton.icon(...),
],
)
按钮使用图标加文字组合,能让用户迅速理解操作含义。
12.4 设置卡片
设置卡片包含 Work 和 Break 两个 Slider:
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
const Text('Settings', style: TextStyle(fontWeight: FontWeight.bold)),
// Work Slider
// Break Slider
],
),
),
)
设置区放在下方是合理的,因为用户启动计时后主要关注进度和按钮,配置不是高频操作。
十三、鸿蒙适配关注点
13.1 为什么适配风险较低
pomodoro_timer 没有使用原生通知、后台服务、文件读写或设备传感器。核心能力来自 Flutter UI 和 Dart 异步逻辑,因此基础适配风险较低。
| 模块 | 是否依赖平台能力 | 适配关注度 |
|---|---|---|
| 倒计时逻辑 | 否 | 中 |
| Slider 设置 | 否 | 中 |
| 圆形进度 | 否 | 低 |
| 图标展示 | 否 | 低 |
| 后台提醒 | 当前未实现 | 高 |
13.2 前台计时与后台提醒
当前项目是前台番茄钟。应用进入后台后,异步延迟任务的表现可能受到系统调度影响。如果要做真正的专注提醒工具,需要进一步设计通知、后台任务或恢复时校准时间。
当前源码没有实现系统级通知和后台计时能力,因此应将它定位为前台可视化番茄钟,而不是完整闹钟应用。
13.3 鸿蒙设备验证点
适配鸿蒙时建议重点验证:
- Start、Stop、Skip 点击反馈是否稳定。
- Slider 拖动是否顺滑,禁用状态是否明显。
- 工作阶段和休息阶段颜色是否清晰。
- 圆形进度是否每秒递减。
- 小屏、横屏、折叠屏下是否出现拥挤或溢出。
13.4 屏幕尺寸适配
页面中进度环固定为 200x200:
SizedBox(
width: 200,
height: 200,
child: CircularProgressIndicator(...),
)
常规手机上表现稳定。如果适配更小屏幕,可以结合 LayoutBuilder 或 MediaQuery 动态调整尺寸。
十四、测试设计与默认测试改造
14.1 当前测试入口
项目中的测试文件仍是默认计数器测试。对于番茄钟应用,更应该测试初始状态、按钮切换、阶段切换和 Slider 行为。
14.2 初始页面测试
可以验证默认状态:
testWidgets('pomodoro timer renders initial state', (WidgetTester tester) async {
await tester.pumpWidget(const MyApp());
expect(find.text('Pomodoro Timer'), findsWidgets);
expect(find.text('Work Session'), findsOneWidget);
expect(find.text('25:00'), findsOneWidget);
expect(find.text('Ready'), findsOneWidget);
expect(find.text('Completed: 0 pomodoros'), findsOneWidget);
});
14.3 Start 状态测试
点击 Start 后,状态应切换为 Focus:
testWidgets('start button changes running state', (WidgetTester tester) async {
await tester.pumpWidget(const MyApp());
await tester.tap(find.text('Start'));
await tester.pump();
expect(find.text('Focus!'), findsOneWidget);
expect(find.text('Stop'), findsOneWidget);
});
14.4 Skip 阶段测试
工作阶段点击 Skip 后,应进入休息阶段,并完成数加 1:
testWidgets('skip work phase enters break phase', (WidgetTester tester) async {
await tester.pumpWidget(const MyApp());
await tester.tap(find.text('Skip'));
await tester.pump();
expect(find.text('Break Time'), findsOneWidget);
expect(find.text('05:00'), findsOneWidget);
expect(find.text('Completed: 1 pomodoros'), findsOneWidget);
});
14.5 倒计时递减测试
可以通过 pump 推进一秒:
testWidgets('timer decreases after one second', (WidgetTester tester) async {
await tester.pumpWidget(const MyApp());
await tester.tap(find.text('Start'));
await tester.pump();
await tester.pump(const Duration(seconds: 1));
expect(find.text('24:59'), findsOneWidget);
});
这些测试能覆盖最关键的业务闭环。
十五、可维护性优化方向
15.1 抽离阶段枚举
当前使用 _isWorkPhase 布尔值描述阶段。若未来增加长休息阶段,可以使用枚举:
enum PomodoroPhase {
work,
shortBreak,
longBreak,
}
枚举比布尔值更适合多阶段业务。
15.2 抽离时间格式化
可以把格式化逻辑抽成纯函数:
String formatMinuteSecond(int remainingSeconds) {
final minutes = remainingSeconds ~/ 60;
final seconds = remainingSeconds % 60;
return '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
}
这样可以直接写单元测试,不依赖 Widget。
15.3 防止重复启动循环
当前正常 UI 交互下,Start 会立即变成 Stop,不容易重复启动。但为了增强健壮性,可以在 _startTimer() 中加保护:
void _startTimer() {
if (_isRunning) return;
setState(() {
_isRunning = true;
});
_runTimer();
}
15.4 使用真实时间校准
如果希望进入后台后仍然更准确,可以记录开始时间和目标结束时间:
DateTime? phaseEndAt;
恢复页面时用当前时间和结束时间计算剩余秒数,而不是完全依赖每秒递减。这对移动端和鸿蒙适配都更稳。
十六、功能扩展方向
16.1 增加长休息
经典番茄工作法通常每完成若干个番茄后进入长休息。可以设计规则:
bool shouldTakeLongBreak(int completedPomodoros) {
return completedPomodoros > 0 && completedPomodoros % 4 == 0;
}
16.2 增加提醒能力
阶段结束后可以加入:
- 页面弹窗。
- 声音提示。
- 震动提醒。
- 系统通知。
其中系统通知和震动可能涉及平台能力,适配鸿蒙时需要结合对应插件和权限策略。
16.3 增加任务名称
可以让用户为当前番茄设置任务名:
String _taskName = 'Focus task';
后续统计时可以记录“完成了哪个任务”。
16.4 增加历史记录
历史记录可以包含:
| 字段 | 含义 |
|---|---|
| 开始时间 | 本次番茄开始时间 |
| 结束时间 | 本次番茄结束时间 |
| 工作时长 | 配置的工作分钟数 |
| 是否完成 | 自然结束或手动跳过 |
| 任务名称 | 用户填写的任务 |
十七、性能与体验优化
17.1 当前性能特点
页面每秒刷新一次,组件数量不多,性能压力很小。主要体验点不是计算速度,而是状态反馈是否清晰。
17.2 控制刷新范围
随着功能扩展,可以把设置区、统计区、进度区拆成独立 Widget,减少 build() 的阅读负担:
class PomodoroProgressCard extends StatelessWidget {
const PomodoroProgressCard({super.key});
Widget build(BuildContext context) {
return const Card(child: SizedBox.shrink());
}
}
17.3 深色模式适配
当前颜色多处直接使用 Colors.red、Colors.green、Colors.orange。深色模式下可以更多依赖主题:
final colorScheme = Theme.of(context).colorScheme;
然后统一从 colorScheme 中取主色、容器色和文本色。
17.4 小屏幕按钮适配
底部两个按钮使用较大的水平内边距:
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16)
如果在小屏设备上出现拥挤,可以用 Wrap 替代 Row,让按钮自动换行。
十八、常见问题与优化建议
18.1 为什么工作结束后完成数才增加
番茄钟统计的是完成的专注单元,所以只有工作阶段结束或跳过工作阶段时,_completedPomodoros 才会加 1。休息阶段结束只是进入下一轮工作,不计入完成数。
18.2 为什么运行中不能调整 Slider
运行中调整工作或休息时长会改变进度环分母,也会影响当前阶段剩余时间语义。禁用 Slider 可以让计时过程更稳定。
18.3 为什么 Skip 会停止运行
_skipPhase() 中会把 _isRunning 设为 false。这样用户跳到下一阶段后,可以确认状态再手动开始,避免误进入下一个计时循环。
18.4 为什么当前没有 Reset 按钮
源码中实现了 _resetTimer(),但当前 UI 只提供 Start/Stop 和 Skip。若产品需要更完整的控制,可以把 Reset 按钮加入操作区。
18.5 鸿蒙适配最应该关注什么
重点关注前台计时稳定性、Slider 触摸体验、页面尺寸适配、阶段切换表现和后台返回后的状态一致性。当前项目没有系统通知和后台提醒能力。
十九、完整流程复盘
19.1 页面启动流程
main()
-> runApp(MyApp)
-> MaterialApp
-> MyHomePage
-> build()
-> 显示 Work Session 和 25:00
19.2 开始计时流程
点击 Start
-> _startTimer()
-> _isRunning = true
-> _runTimer()
-> 每秒递减 _remainingSeconds
-> UI 重新显示时间和进度
19.3 工作阶段结束流程
_remainingSeconds 归零
-> _isRunning = false
-> _completedPomodoros++
-> _isWorkPhase = false
-> _remainingSeconds = _breakDuration * 60
-> 页面切换为 Break Time
19.4 休息阶段结束流程
_remainingSeconds 归零
-> _isRunning = false
-> _isWorkPhase = true
-> _remainingSeconds = _workDuration * 60
-> 页面切换为 Work Session
二十、相关资源与继续学习
20.1 Flutter 学习资源
番茄钟涉及 Widget、异步、进度组件和测试,可以结合以下资源学习:
| 资源 | 内容 |
|---|---|
| Flutter Docs | Flutter 官方开发文档 |
| Dart async | Dart 异步编程 |
| Widget catalog | Flutter 常用组件索引 |
| Flutter testing | Widget 测试与交互模拟 |
20.2 番茄钟扩展方向
后续可以继续增强:
- 长休息规则。
- 阶段完成弹窗。
- 声音和震动提醒。
- 本地历史记录。
- 每日专注目标。
- 深色模式。
- 后台恢复校准。
20.3 跨端实践价值
pomodoro_timer 很适合作为 Flutter 适配鸿蒙的小型样例。它没有复杂原生依赖,但包含异步状态、控件交互、进度绘制和页面滚动,能覆盖很多实际应用都会遇到的基础问题。
总结
pomodoro_timer 用简洁的 Flutter 代码实现了一个可配置的番茄钟。它通过 _workDuration 和 _breakDuration 描述配置,通过 _remainingSeconds 驱动倒计时和进度环,通过 _isWorkPhase 区分工作与休息阶段,通过 _completedPomodoros 记录完成数量。
从工程角度看,这个项目最值得学习的是阶段状态迁移:工作结束进入休息并增加完成数,休息结束回到工作阶段。面向鸿蒙适配时,项目依赖较轻,主要需要验证前台计时、Slider 触摸、页面尺寸和生命周期表现。对于想掌握 Flutter 工具类应用开发的开发者来说,它是一个清晰、实用且容易扩展的案例。
如果这篇文章对你有帮助,欢迎点赞、收藏、关注,你的支持是我持续创作的动力!
相关资源:
更多推荐



所有评论(0)