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 实战能力:

  1. 如何用状态字段描述计时器。
  2. 如何用异步循环实现每秒递减。
  3. 如何通过 ChoiceChip 选择预设时长。
  4. 如何用 CircularProgressIndicator 表示剩余进度。
  5. 如何在异步任务中使用 mounted 保护 UI 更新。
  6. 如何面向鸿蒙验证前台计时、弹窗和触摸反馈。

二、工程结构与运行方式

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 计时器类应用的开发者来说,它是一个清晰、实用且容易扩展的案例。

如果这篇文章对你有帮助,欢迎点赞、收藏、关注,你的支持是我持续创作的动力!


相关资源:

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐