Flutter 实战:meditation_timer 冥想计时器的预设时长、进度环与鸿蒙适配解析
Flutter 实战:meditation_timer 冥想计时器的预设时长、进度环与鸿蒙适配解析
前言
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
meditation_timer 是一个基于 Flutter 实现的冥想计时器。它支持 5、10、15、20、30 分钟预设时长,提供开始、暂停、重置操作,通过圆形进度环展示剩余时间,并在完成后弹出提示,同时累计完成会话次数。
本文基于项目真实源码展开,重点分析 预设时长选择、Future.doWhile 倒计时循环、圆形进度计算、完成弹窗、会话统计、冥想提示列表 和 鸿蒙适配关注点。文章内容可直接发布到 CSDN,不包含面向作者的检查说明。
冥想计时器看似只是倒计时,但它比普通计时器多了“安静、稳定、可预期”的产品语义。代码上要做到状态清晰、进度准确、暂停可控、完成反馈明确。

图示说明:本文围绕 Flutter 冥想计时器的时长选择、倒计时循环、进度展示和跨端适配展开,适合用于鸿蒙、Android、iOS 等多端小工具应用开发复盘。
一、项目定位与功能概览
1.1 应用主题
meditation_timer 的定位是一个 前台冥想倒计时工具。用户选择一个冥想时长,点击 Start 开始倒计时,运行中可以暂停,必要时可以重置。倒计时完成后,应用会弹出完成提示并累计一次会话。
核心功能如下:
| 功能 | 页面表现 | 源码实现 |
|---|---|---|
| 预设时长 | ChoiceChip 选择 5/10/15/20/30 | _presetDurations |
| 开始计时 | Start 按钮 | _startTimer() |
| 暂停计时 | Pause 按钮 | _stopTimer() |
| 重置计时 | Reset 按钮 | _resetTimer() |
| 剩余时间 | mm:ss 格式 |
_formattedTime |
| 圆形进度 | CircularProgressIndicator |
_remainingSeconds / (_duration * 60) |
| 完成统计 | Sessions: x |
_completedSessions |
| 完成提示 | AlertDialog | _showCompletionDialog() |
1.2 当前实现边界
当前源码只实现前台倒计时和 UI 提示,没有实现音频、震动、后台计时或系统通知。
| 能力 | 当前是否实现 | 说明 |
|---|---|---|
| 前台倒计时 | 是 | 通过 Future.doWhile |
| 预设时长 | 是 | 5 到 30 分钟 |
| 完成弹窗 | 是 | AlertDialog |
| 会话统计 | 是 | 内存计数 |
| 背景音乐 | 否 | 没有音频插件 |
| 后台提醒 | 否 | 没有系统通知 |
| 持久化统计 | 否 | 重启后次数不保留 |
1.3 适合学习的点
这个项目适合学习以下 Flutter 实战能力:
- 如何用状态字段描述计时器。
- 如何用异步循环实现每秒递减。
- 如何通过 ChoiceChip 选择预设时长。
- 如何用 CircularProgressIndicator 表示剩余进度。
- 如何在异步任务中使用
mounted保护 UI 更新。 - 如何面向鸿蒙验证前台计时、弹窗和触摸反馈。
二、工程结构与运行方式
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
这说明倒计时、弹窗、提示列表和进度环都由 Flutter 标准能力完成。
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 根组件
根组件负责创建 MaterialApp:
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
title: 'Meditation Timer',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.purple),
),
home: const MyHomePage(title: 'Meditation Timer'),
);
}
}
这里有三个关键信息:
- 应用标题是
Meditation Timer。 - 主题种子色是
Colors.purple。 - 首页是
MyHomePage。
3.3 主题色选择
紫色经常用于安静、冥想、专注类界面。源码中图标、进度环、按钮和统计卡片都围绕紫色展开,形成统一的冥想氛围。
当前源码没有显式设置
useMaterial3: true,因此文章以真实代码为准。如果后续需要统一 Material 3 表现,可以在ThemeData中补充该配置。
四、状态字段设计
4.1 核心状态总览
计时器状态集中在 _MyHomePageState 中:
int _duration = 10;
int _remainingSeconds = 600;
bool _isRunning = false;
int _completedSessions = 0;
final List<int> _presetDurations = [5, 10, 15, 20, 30];
4.2 字段说明
| 字段 | 类型 | 默认值 | 作用 |
|---|---|---|---|
_duration |
int |
10 | 当前冥想时长,单位分钟 |
_remainingSeconds |
int |
600 | 当前剩余秒数 |
_isRunning |
bool |
false | 是否正在计时 |
_completedSessions |
int |
0 | 已完成会话次数 |
_presetDurations |
List<int> |
5/10/15/20/30 | 可选预设时长 |
4.3 状态分层
可以把状态分为三类:
| 状态类别 | 字段 | 说明 |
|---|---|---|
| 配置状态 | _duration、_presetDurations |
冥想时长选择 |
| 运行状态 | _remainingSeconds、_isRunning |
倒计时过程 |
| 统计状态 | _completedSessions |
完成次数 |
4.4 初始状态
默认时长为 10 分钟,所以 _remainingSeconds 初始是 600。initState() 中也会再次同步:
void initState() {
super.initState();
_remainingSeconds = _duration * 60;
}
五、预设时长选择
5.1 setDuration 方法
选择时长由 _setDuration() 控制:
void _setDuration(int minutes) {
if (_isRunning) return;
setState(() {
_duration = minutes;
_remainingSeconds = _duration * 60;
});
}
5.2 运行中不能修改
方法开头判断:
if (_isRunning) return;
这能避免倒计时过程中修改总时长导致进度计算不稳定。
5.3 ChoiceChip 渲染
预设时长通过 ChoiceChip 展示:
Wrap(
spacing: 8,
children: _presetDurations.map((d) {
return ChoiceChip(
label: Text('$d'),
selected: _duration == d,
onSelected: (_) => _setDuration(d),
selectedColor: Colors.purple.shade200,
);
}).toList(),
)
5.4 预设时长表
| 选项 | 秒数 |
|---|---|
| 5 分钟 | 300 |
| 10 分钟 | 600 |
| 15 分钟 | 900 |
| 20 分钟 | 1200 |
| 30 分钟 | 1800 |
六、开始、暂停与重置
6.1 开始计时
开始按钮触发 _startTimer():
void _startTimer() {
setState(() {
_isRunning = true;
});
_runTimer();
}
6.2 暂停计时
暂停按钮触发 _stopTimer():
void _stopTimer() {
setState(() {
_isRunning = false;
});
}
按钮文案显示为 Pause,但实现本质是停止当前循环,保留剩余时间。再次点击 Start 会继续从剩余时间开始。
6.3 重置计时
重置按钮触发 _resetTimer():
void _resetTimer() {
setState(() {
_isRunning = false;
_remainingSeconds = _duration * 60;
});
}
6.4 控制按钮表
| 状态 | 主按钮文案 | 行为 |
|---|---|---|
| 未运行 | Start | 开始计时 |
| 运行中 | Pause | 暂停计时 |
| 任意状态 | Reset | 回到当前时长 |
七、Future.doWhile 倒计时循环
7.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;
_completedSessions++;
_showCompletionDialog();
}
});
return _isRunning;
});
}
7.2 循环流程
进入 Future.doWhile
-> 如果未运行,结束
-> 等待 1 秒
-> 如果组件已卸载,结束
-> 剩余秒数大于 0,递减
-> 否则停止、累计次数、显示完成弹窗
-> 根据 _isRunning 判断是否继续
7.3 mounted 保护
异步等待后,组件可能已经离开页面。源码使用:
if (!mounted) return false;
这能避免页面销毁后继续调用 setState()。
7.4 运行边界
当前项目是前台倒计时器。如果应用进入后台,异步延迟任务可能受到系统调度影响。正式冥想应用若需要后台提醒,应另行设计。
八、时间格式化
8.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')}';
}
8.2 整除和取余
| 表达式 | 作用 |
|---|---|
_remainingSeconds ~/ 60 |
得到分钟 |
_remainingSeconds % 60 |
得到秒 |
8.3 补零显示
padLeft(2, '0') 能让 5 显示为 05:
seconds.toString().padLeft(2, '0')
8.4 显示示例
| 剩余秒数 | 显示 |
|---|---|
| 600 | 10:00 |
| 125 | 02:05 |
| 9 | 00:09 |
九、圆形进度环
9.1 CircularProgressIndicator
圆形进度环配置如下:
CircularProgressIndicator(
value: _remainingSeconds / (_duration * 60),
strokeWidth: 12,
backgroundColor: Colors.grey.shade200,
valueColor: const AlwaysStoppedAnimation<Color>(Colors.purple),
)
9.2 进度公式
progress = remainingSeconds / totalSeconds
9.3 进度示例
| 总时长 | 剩余时长 | 进度 |
|---|---|---|
| 600 秒 | 600 秒 | 1.0 |
| 600 秒 | 300 秒 | 0.5 |
| 600 秒 | 0 秒 | 0 |
9.4 Stack 布局
进度环和时间文本通过 Stack 居中叠放:
Stack(
alignment: Alignment.center,
children: [
SizedBox(
width: 200,
height: 200,
child: CircularProgressIndicator(...),
),
Column(
children: [
Text(_formattedTime),
Text(_isRunning ? 'Breathe...' : 'Ready'),
],
),
],
)
十、完成弹窗与会话统计
10.1 完成时机
当 _remainingSeconds 变为 0 后:
_isRunning = false;
_completedSessions++;
_showCompletionDialog();
10.2 完成弹窗
弹窗内容如下:
void _showCompletionDialog() {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Meditation Complete'),
content: const Text('Great job! You completed your meditation session.'),
actions: [
ElevatedButton(
onPressed: () {
Navigator.pop(context);
_resetTimer();
},
child: const Text('Done'),
),
],
),
);
}
10.3 统计卡片
完成次数通过卡片展示:
Text(
'Sessions: $_completedSessions',
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.purple,
),
)
10.4 统计边界
当前完成次数只保存在内存中。应用重启后次数会恢复为 0。
十一、页面布局与视觉层级
11.1 页面整体结构
页面使用 SingleChildScrollView:
body: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Column(...),
)
这对小屏设备和横屏场景很友好。
11.2 主计时卡片
主卡片包含图标、进度环和时间:
Card(
child: Padding(
padding: const EdgeInsets.all(32),
child: Column(...),
),
)
11.3 冥想图标
图标根据运行状态变化颜色:
Icon(
Icons.self_improvement,
size: 64,
color: _isRunning ? Colors.purple : Colors.grey,
)
11.4 状态文案
中心文案根据运行状态变化:
| 状态 | 文案 |
|---|---|
| 未运行 | Ready |
| 运行中 | Breathe… |
十二、冥想提示列表
12.1 Tips 卡片
页面底部有提示卡片:
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
const Text('Tips', style: TextStyle(fontWeight: FontWeight.bold)),
_buildTip('Find a quiet, comfortable place'),
],
),
),
)
12.2 buildTip 方法
提示行由 _buildTip() 构建:
Widget _buildTip(String text) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
children: [
Icon(Icons.check, size: 16, color: Colors.purple.shade300),
const SizedBox(width: 8),
Text(text, style: TextStyle(color: Colors.grey.shade700)),
],
),
);
}
12.3 提示内容
源码中包含四条提示:
| 提示 |
|---|
| Find a quiet, comfortable place |
| Sit with your back straight |
| Focus on your breathing |
| Let thoughts pass without judgment |
12.4 体验价值
提示内容不影响计时逻辑,但能增强冥想场景感,让工具更像一个完整小应用。
十三、运行中隐藏时长选择
13.1 条件渲染
时长选择只在未运行时展示:
if (!_isRunning) ...[
const Text('Duration (minutes)'),
Wrap(...),
]
13.2 为什么运行中隐藏
运行中隐藏时长选择可以避免用户误改总时长,导致进度环比例和剩余时间语义混乱。
13.3 UI 简化
运行中页面只保留最重要的信息:
- 剩余时间。
- Breathe 状态。
- Pause 和 Reset。
13.4 产品语义
冥想过程强调减少干扰。隐藏非必要配置项也符合这个场景。
十四、鸿蒙适配关注点
14.1 为什么适配风险较低
meditation_timer 主要由 Flutter 标准组件和 Dart 异步逻辑构成,不依赖音频、通知、震动或系统后台任务,因此基础适配风险较低。
| 模块 | 是否依赖平台能力 | 适配关注度 |
|---|---|---|
| 前台计时 | Dart 异步逻辑 | 中 |
| 圆形进度 | Flutter 标准组件 | 低 |
| ChoiceChip | Flutter 标准组件 | 低 |
| 完成弹窗 | Flutter 标准组件 | 中 |
| 后台提醒 | 当前未实现 | 高 |
14.2 前台计时验证
鸿蒙设备上需要重点验证:
- Start 后秒数是否递减。
- Pause 后是否停止递减。
- Reset 是否恢复当前时长。
- 完成时是否弹出对话框。
14.3 屏幕尺寸适配
页面使用 SingleChildScrollView,能适应不同屏幕高度。仍需验证圆形进度环在小屏、横屏和折叠屏上的显示。
14.4 后台能力边界
当前项目没有后台计时、系统通知和音频提醒能力。若要做正式冥想工具,需要单独设计这些能力。
当前项目适合作为 Flutter 冥想计时器 UI 与前台逻辑样例,不应理解为完整的冥想音频或后台提醒应用。
十五、测试设计与默认测试改造
15.1 当前测试入口
项目中的测试文件仍是默认计数器测试。对于冥想计时器,更有价值的是验证初始状态、时长选择、开始暂停和完成逻辑。
15.2 初始页面测试
testWidgets('meditation timer renders initial state', (WidgetTester tester) async {
await tester.pumpWidget(const MyApp());
expect(find.text('Meditation Timer'), findsWidgets);
expect(find.text('10:00'), findsOneWidget);
expect(find.text('Ready'), findsOneWidget);
expect(find.text('Sessions: 0'), findsOneWidget);
});
15.3 时长选择测试
testWidgets('can select duration', (WidgetTester tester) async {
await tester.pumpWidget(const MyApp());
await tester.tap(find.text('5'));
await tester.pump();
expect(find.text('05:00'), findsOneWidget);
});
15.4 开始暂停测试
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('Breathe...'), findsOneWidget);
expect(find.text('Pause'), findsOneWidget);
});
15.5 重置测试
testWidgets('reset restores selected duration', (WidgetTester tester) async {
await tester.pumpWidget(const MyApp());
await tester.tap(find.text('Reset'));
await tester.pump();
expect(find.text('10:00'), findsOneWidget);
});
十六、可维护性优化方向
16.1 抽离计时状态
当前状态直接放在 State 中,适合小项目。后续可以抽成模型:
class MeditationState {
const MeditationState({
required this.duration,
required this.remainingSeconds,
required this.isRunning,
});
final int duration;
final int remainingSeconds;
final bool isRunning;
}
16.2 防止重复启动循环
当前正常 UI 下不容易重复启动,但可以在 _startTimer() 中增加保护:
void _startTimer() {
if (_isRunning) return;
setState(() {
_isRunning = true;
});
_runTimer();
}
16.3 使用真实时间校准
如果要提高后台恢复后的准确性,可以记录开始时间和结束时间,用当前时间计算剩余秒数,而不是只靠每秒递减。
16.4 增加持久化
如果希望完成次数保留,可以把 _completedSessions 持久化到本地。
十七、功能扩展方向
17.1 增加背景音
可以增加白噪音、雨声或钟声,但这需要音频插件支持。
17.2 增加震动或通知
完成时可以增加震动或系统通知,但需要平台能力和权限适配。
17.3 增加自定义时长
除了预设时长,也可以支持用户输入任意分钟数。
17.4 增加历史统计
可以记录每天冥想次数和总时长,形成统计面板。
十八、常见问题与优化建议
18.1 为什么运行中不能改时长
因为进度环依赖 _remainingSeconds / (_duration * 60),运行中修改时长会让进度比例含义变得不清晰。
18.2 为什么 Pause 后还能继续
Pause 只是把 _isRunning 设为 false,并没有重置 _remainingSeconds,所以再次 Start 会从剩余时间继续。
18.3 为什么完成后会自动累计 Sessions
当剩余时间归零时,源码执行 _completedSessions++,表示完成了一次冥想会话。
18.4 为什么没有声音提示
当前源码没有引入音频插件,也没有调用系统通知或震动能力,因此完成反馈仅是弹窗。
18.5 鸿蒙适配最应该关注什么
重点关注前台计时稳定性、弹窗显示、ChoiceChip 触摸反馈、横竖屏布局和后台返回后的表现。当前项目没有后台提醒能力。
十九、完整流程复盘
19.1 页面启动流程
main()
-> runApp(MyApp)
-> MaterialApp
-> MyHomePage
-> initState 设置剩余秒数
-> build 渲染计时器
19.2 选择时长流程
点击 ChoiceChip
-> _setDuration(minutes)
-> 判断是否运行中
-> 更新 duration
-> 更新 remainingSeconds
-> 刷新时间和进度
19.3 开始计时流程
点击 Start
-> _startTimer()
-> _isRunning = true
-> _runTimer()
-> 每秒递减 remainingSeconds
-> 刷新进度环和时间
19.4 完成流程
remainingSeconds 归零
-> _isRunning = false
-> _completedSessions++
-> showDialog 显示完成提示
-> 点击 Done 后重置
二十、相关资源与继续学习
20.1 Flutter 学习资源
冥想计时器涉及异步、进度条、芯片选择和弹窗,可以结合以下资源学习:
| 资源 | 内容 |
|---|---|
| Flutter Docs | Flutter 官方开发文档 |
| Dart async | Dart 异步编程 |
| Widget catalog | Flutter 常用组件 |
| Flutter testing | Widget 测试与交互模拟 |
20.2 冥想应用扩展方向
后续可以继续增强:
- 背景音。
- 钟声提醒。
- 自定义时长。
- 历史统计。
- 连续冥想天数。
- 深色模式。
- 后台提醒。
20.3 跨端实践价值
meditation_timer 很适合作为 Flutter 适配鸿蒙的小型计时器样例。它依赖很轻,但覆盖了异步倒计时、圆形进度、芯片选择、弹窗提示、滚动布局和触摸反馈,能帮助开发者验证很多跨端基础能力。
总结
meditation_timer 用简洁的 Flutter 代码实现了一个前台冥想倒计时器。它通过 _duration 和 _remainingSeconds 管理时长,通过 _isRunning 控制运行状态,通过 Future.doWhile 每秒递减,通过 CircularProgressIndicator 展示进度,并在完成后累计会话次数和展示弹窗。
从工程角度看,这个项目最值得学习的是“预设时长 + 异步倒计时 + 完成反馈”的组合方式。面向鸿蒙适配时,项目依赖较轻,主要需要验证前台计时、弹窗、布局和触摸反馈。对于想学习 Flutter 计时器类应用的开发者来说,它是一个清晰、实用且容易扩展的案例。
如果这篇文章对你有帮助,欢迎点赞、收藏、关注,你的支持是我持续创作的动力!
相关资源:
更多推荐


所有评论(0)