Merack

  • About
Merack
崭新万物正上升幻灭如明星
我却乌云遮目
  1. 首页
  2. Code
  3. 正文

用Flutter开发的一款基于随机奖励机制的计时专注app

2025-05-28

灵感来源于之前看到的一个B站视频: BV1naLozQEBq  , 觉得挺有意思的, 联想到很久之前学的Flutter也没有怎么练习过, 于是本着复习Flutter的想法断断续续捣鼓出的一个玩具.

机制简单介绍

  1. 有一个专注时间和休息时间, 专注时间一到就开始进入休息时间, 休息时间结束完成一个周期
  2. 定义一个区间, 每次生成一个在这个区间的随机秒数, 每经过这么多秒就在专注时间内插入一个微休息, 微休息时间很短大概是10s
  3. 微休息单独计时, 不影响主专注时间的计时且只发生在专注阶段

原理引用下GitHub上JokerQianwei/Focus  项目的README:

间隔效应 (Gap Effects):神经科学家 Andrew Huberman 教授指出,学习过程中短暂的、几秒钟的停顿,可以触发大脑神经元的快速回顾机制,有效提升学习和记忆效率。Random Focus 的随机提示音和小憩功能正是基于此原理,引导用户利用碎片化的时间进行高效的信息巩固。(参考:Huberman Lab 播客 - 像天才一样学习,1:22:03 处)

随机奖励 (Random Rewards):心理学研究表明,不可预测的随机奖励比固定奖励更能激发持续的行为动力。本应用中的随机提示音,在提醒休息的同时,也扮演了积极反馈的角色,帮助用户克服长时间专注带来的疲惫感,保持学习或工作的动力。(灵感来源:为什么我能每天学习10小时)

还可以在这个链接了解: https://www.yuque.com/u43692620/yyl2g7/fup4ss9g56olg3gy

成品展示

下载地址(仅安卓):

夸克链接:https://pan.quark.cn/s/3746722fbdd9
提取码:Uz26
====================
蓝奏
https://merack.lanzoum.com/b0187j28he
密码:e57g

更新: https://github.com/Merack/time_machine/releases

代码设计

总体思路

Dart 里的Timer类正好提供了来做倒计时的方法periodic, 函数签名: factory Timer.periodic(Duration duration, void callback(Timer timer)). 实际上它的作用是每隔一个duration周期执行一次回调方法callback, 那么我们就可以让这个周期为1s, 用变量存储需要倒计时的秒数, 在callback里一直让这个变量做-1操作, 直到为0就可以了.核心逻辑如下:

void _startFocusCountdown() {
    _timer = Timer.periodic(const Duration(seconds: 1), (timer) {
      if (remainingFocusTime > 0) {
        remainingFocusTime--;
      } else {
        _handleFocusTimerComplete();
      }
    });
  }

由于专注和微休息是单独计时, 所以创建两个Timer来计时, 一个用作专注与休息, 一个用作微休息的到来的倒计时与微休息期间的倒计时.

状态维护

为了让应用知道现在处于什么阶段了, 我们还需要维护当前的状态信息. 目前是设计了5个状态: 专注状态, 大休息状态, 微休息状态, 暂停状态, 停止状态.

用了一个枚举类来保存, 同时isRunning变量用来标识当前专注计时器是否处于运行状态

enum TimerStatus {
  focus,      // 专注状态
  microBreak, // 微休息状态
  bigBreak,   // 大休息状态
  paused,     // 暂停状态
  stopped     // 停止状态
}

// 是否正在运行
var isRunning = false

状态切换

在当前状态计时结束后要进入下一个阶段, 执行相关操作, 因此定义了一系列操作方法.

1.重置

void resetTimer() {
   // 停止两个计时器
    _timer?.cancel();
    _microBreakTimer?.cancel();
    // 状态更新为停止
    timerStatus = TimerStatus.stopped;
    // 重置剩余时间和总时间
    remainingFocusTime = state.focusTimeSeconds;
    totalTime = state.focusTimeSeconds;
    // 更新运行状态
    isRunning = false;
    // 生成下一个微休息时间
    generateNextMicroBreakInterval();
}

2.开始

/// 开始计时器
  void _startTimer() {
    if (timerStatus == TimerStatus.stopped) {
      // 首次启动,直接进入专注会话
      _startFocusSession();
    } else {
      // 从暂停状态恢复
      if (timerStatus == TimerStatus.paused) {
        // 更新 isRunning 状态
        isRunning = true;
        // 如果之前的状态是 focus 或者 microBreak, 则恢复为专注状态
        if ((previousStatus == TimerStatus.focus) ||
            (previousStatus == TimerStatus.microBreak)) {
          timerStatus = TimerStatus.focus;
        } else {
          // 恢复为之前的状态, 逻辑上这里应该只能是 bigBreak
          timerStatus = previousStatus;
        }
        // 重新开始专注计时器
        _startFocusCountdown();
        // 重新生成随机间隔
        generateNextMicroBreakInterval();
        // 重新开始微休息计时器
        _startMicroBreakCountdown();
      }
    }
  }

3. 暂停

/// 暂停计时器
  void _pauseTimer() {
    // 一同暂停专注和微休息
    _timer?.cancel();
    _microBreakTimer?.cancel();
    // 保存之前的状态用于恢复
    previousStatus = timerStatus;
    // 状态更新
    timerStatus = TimerStatus.paused;
    isRunning = false;
  }

专注计时器相关

1. 专注计时器计数逻辑, 专注计时和休息计时共用

void _startFocusCountdown() {
  _timer = Timer.periodic(const Duration(seconds: 1), (timer) {
    if (remainingFocusTime > 0) {
      remainingFocusTime--;
    } else {
    // 处理专注计时器完成的函数
      _handleFocusTimerComplete();
    }
  });
}

2. 处理专注计时器完成的函数, 专注计时和休息计时共用

void _handleFocusTimerComplete() {
   _timer?.cancel();

   switch (timerStatus) {
     case TimerStatus.focus:
       _completeFocusStatus();
       break;
     // 如果专注计时走完正好又处于微休息期间时, 仍然认为完成了专注任务
     case TimerStatus.microBreak:
       _completeFocusStatus();
       break;
     case TimerStatus.bigBreak:
       _completeBigBreak();
       break;
     default:
       break;
   }
 }

3.开始专注会话

/// 开始专注会话
  void _startFocusSession() {
    // 初始化状态
    timerStatus = TimerStatus.focus;
    remainingFocusTime = focusTimeSeconds;
    totalTime = focusTimeSeconds;
    isRunning = true;
    generateNextMicroBreakInterval();
   // 同时开启两个计时器
    _startFocusCountdown();
    _startMicroBreakCountdown();
  }

4. 处理专注状态完成

/// 完成专注会话
  void _completeFocusStatus() {
    // 不在专注时段, 微休息停止计时
    _microBreakTimer?.cancel();

    // 播放专注完成音效
    _playAudio('audio/wakeup.mp3');

    // 增加完成专注数
    state.completedCycles.value++;

    // 如果用户设置大休息时间为0, 则跳过休息阶段
    if (state.bigBreakTimeSeconds.value == 0) {
      resetTimer();
      _startFocusSession();
      return;
    }

    // 进入大休息状态
    state.timerStatus.value = TimerStatus.bigBreak;
    state.remainingFocusTime.value = state.bigBreakTimeSeconds.value;
    state.totalTime.value = state.bigBreakTimeSeconds.value;

    // 开始大休息倒计时
    _startFocusCountdown();
  }

5.处理休息状态完成

但休息完成时就说明一个周期完成了, 于是进入下一个周期_startFocusSession() 或者重置一切让计时器停止resetTimer(), 后期再加个flag控制这个行为吧

/// 完成大休息
  void _completeBigBreak() {
    // 播放大休息结束音效
    _playAudio('audio/alarm-wood.mp3');
    // 完成了一个周期, 开始新的专注会话
    // _startFocusSession();
    // 重置计数器
    resetTimer();
  }

微休息计时器相关

微休息会话的开启其实是由前面的开始专注会话_startFocusSession()里随专注计时器一同开启的, 因此微休息这里只要处理微休息计数器逻辑和微休息状态完成的逻辑就可以了. 但是微休息来临计时的完成和微休息的完成都没有像专注和大休息一样分开写相应的complete函数, 而是都把complete的逻辑分散到_startMicroBreakCountdown 和_handleMicroBreakStatus里了, 所以这部分看起来有点屎山, 后面有空再优化

1.处理微休息到来间隔计时和微休息期间计时

void _startMicroBreakCountdown() {
    // 检查微休息是否启用
    if (!microBreakEnabled) {
      return;
    }

    // 如果微休息时间设置为0也不启用微休息
    if (microBreakTimeSeconds == 0) {
      return;
    }
    // 处理微休息到来时间倒计时
    if (timerStatus == TimerStatus.focus) {
      _microBreakTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
        if (nextMicroBreakTime > 0) {
          nextMicroBreakTime--;
        } else {
        // 处理微休息计数器计时完成逻辑
          _handleMicroBreakStatus(isStartMicroBreak: true);
        }
      });
    }
    // 处理微休息期间倒计时
    if (timerStatus == TimerStatus.microBreak) {
      _microBreakTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
        if (remainingMicroBreakTime > 0) {
          remainingMicroBreakTime--;
        } else {
          _handleMicroBreakStatus(isStartMicroBreak: false);
        }
      });
    }
  }

2. 处理微休息计数器计时完成逻辑

void _handleMicroBreakStatus({required bool isStartMicroBreak}) {
    // 开始微休息
    if (isStartMicroBreak) {
      _microBreakTimer?.cancel();

      // 播放微休息开始音效
      _playAudio('audio/drop.mp3');

      // 进入微休息状态
      timerStatus = TimerStatus.microBreak;

      // 开始微休息倒计时
      // 设置微休息时间
      remainingMicroBreakTime = microBreakTimeSeconds;
      _startMicroBreakCountdown();
    } else {
      // 微休息结束
      _microBreakTimer?.cancel();

      // 播放微休息结束音效
      _playAudio('audio/ding.mp3');

      // 更新状态
      timerStatus = TimerStatus.focus;

      generateNextMicroBreakInterval();
      // 开始微休息倒计时
      _startMicroBreakCountdown();
    }
  }

代码都是随想随写没什么技术含量, 包含界面部分的完整代码等后面再整理整理好就开源出来吧~

更新: 已开源: https://github.com/Merack/time_machine

后续计划

  • 数据统计
  • 自动开始控制
  • 自定义进度条颜色
  • 暗色模式
  • 禅模式
  • 开关提示音
  • 后台保活(尽量...)
  • 多语言--英文

后记

Flutter是跨平台的框架, 但是我只有安卓设备于是只做了安卓的. 而且也只是验证想法的练手作品也别指望它能有多好用, 我只是个菜鸡安卓杀后台的问题解决不了一点~

Flutter的声明式UI写界面确实比安卓原生开发用xml那套方便许多, 但页面元素一多起来层层嵌套也是很折磨人, 经常找括号和逗号要找半天. 体验下来觉得写UI还得是web三件套,怪不得Electron和Tauri这类的框架能这么流行. 那在移动端最接近web的应该是react native了吧, 听说JSI和新架构让rn性能有了不错的提升, 有机会再试试吧, 顺便捡一下学了但从没用过的react.

还有一点就是感觉Flutter有点太依赖生态了, 一些系统调用的功能如果没有相应的包那就得自己写FFI, rn应该也差不多, 不是原生的框架应该都这样吧~

本作品采用 知识共享署名 4.0 国际许可协议 进行许可
标签: flutter
最后更新:2025-06-15

Merack

起身向荒原

点赞
< 上一篇
下一篇 >
文章目录
  • 机制简单介绍
  • 成品展示
  • 代码设计
    • 总体思路
    • 状态维护
    • 状态切换
  • 后续计划
  • 后记

COPYRIGHT © 2024 Merack. ALL RIGHTS RESERVED.

Theme Kratos Made By Seaton Jiang

cloudflare upyun 提供CDN服务