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.doWhileFuture.delayed 如何实现秒级循环
  • _completedPomodoros 如何统计完成的番茄数
  • 完成弹窗如何引导进入短休息或长休息
  • _totalMinutes 当前统计逻辑的真实问题
  • OpenHarmony 适配时需要重点验证哪些 UI 和异步行为

一、项目背景与目标

1.1 项目定位

pomodoro_timer 是一个轻量番茄钟工具,适合用来练习 Flutter 中的局部状态、计时逻辑和工具类页面设计。它没有接入通知、后台保活、音频提醒或本地持久化,主要聚焦前台页面内的倒计时体验。

从用户视角看,应用流程是:

  1. 选择 Work、Short Break 或 Long Break 模式。
  2. 点击 Start 开始倒计时。
  3. 运行中点击 Pause 暂停。
  4. 点击 Reset 重置当前模式时间。
  5. Work 模式结束后弹出完成提示。
  6. 根据完成次数选择短休息或长休息。

从工程视角看,它对应的是:

  1. 使用 _selectedMode 表示当前模式。
  2. 使用 _remainingSeconds 表示剩余秒数。
  3. 使用 _isRunning 表示计时器是否运行。
  4. 使用异步循环每秒更新状态。
  5. 使用 _completedPomodoros 记录完成番茄数。
  6. 使用 showDialog 展示工作完成提示。
  7. 使用颜色列表让界面随模式变化。

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

这里需要关注两点:

  1. 项目没有引入计时器、通知或持久化三方插件。
  2. 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'),
    );
  }
}

它完成了三件事:

  1. 设置应用标题为 Pomodoro Timer
  2. 使用红色作为 Material 3 种子色。
  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;
  });
}

这个方法的核心是:

  1. 先把 _isRunning 设置为 true
  2. 进入 Future.doWhile 异步循环。
  3. 每次等待 1 秒。
  4. 如果页面已卸载或暂停,则停止循环。
  5. 每秒减少剩余时间。
  6. 时间结束后停止,并在 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 中取消计时器对象。它依赖 _isRunningmounted 让异步循环停止。

这种方案适合前台轻量演示,但不适合严格计时场景。真实生产级番茄钟通常需要考虑:

  • 应用进入后台后的时间流逝。
  • 系统暂停任务后的补偿。
  • 通知提醒。
  • 屏幕熄灭后恢复。
  • 以真实时间戳计算剩余时间。

七、暂停与重置逻辑

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();
  });
}

它会:

  1. 更新当前模式。
  2. 停止正在运行的计时。
  3. 按新模式重置剩余时间。

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 设备上需要确认:

  1. 图标是否正常显示。
  2. 图标颜色是否符合预期。
  3. 图标与按钮文字是否对齐。
  4. 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

适合覆盖的行为包括:

  1. 初始显示 25:00
  2. 点击 Short Break 后显示 05:00
  3. 点击 Long Break 后显示 15:00
  4. Start 后按钮切换为 Pause。
  5. Pause 后按钮切换回 Start。
  6. 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 手动验证流程

手动验证可以按如下顺序进行:

  1. 启动应用,确认默认 Work 模式显示 25:00
  2. 点击 Short Break,确认显示 05:00 且主题变绿。
  3. 点击 Long Break,确认显示 15:00 且主题变蓝。
  4. 点击 Start,确认每秒递减。
  5. 点击 Pause,确认倒计时停止。
  6. 点击 Reset,确认恢复当前模式初始时间。
  7. Work 模式倒计时结束后,确认弹出完成提示。
  8. 点击 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.doWhileFuture.delayed 实现秒级循环,通过 _completedPomodoros 统计完成数量,并用 AlertDialog 在工作周期结束后引导用户休息。

从 OpenHarmony 适配角度看,这个项目覆盖了 Material 主题、动态颜色、手势点击、按钮状态切换、圆形 UI、等宽时间文本、异步倒计时、弹窗和 Material Icons 等基础能力,很适合用来验证 Flutter 前台工具类应用在 OpenHarmony 上的表现。

当前源码也有几个真实边界:专注时长统计字段 _totalMinutes 实际按秒递增却按小时和分钟展示,_resetTimer_selectMode 中存在嵌套 setState,休息模式结束没有弹窗提醒,也没有处理后台计时和统计持久化。这些边界不影响项目作为番茄钟入门实战的价值,但在继续工程化时需要优先处理。

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


相关资源:

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐