Flutter Circular Countdown Timer 后台持续运行不停止实现问题咨询
Flutter多计时器后台持续运行解决方案
问题本质
- 你当前使用的
CircularCountDownTimer控件绑定在ViewPromoters页面的State上,按下返回键页面出栈被销毁时,State连带计时器控制器一起被回收,计时器自然停止运行 - 目前你只初始化了1个
CountDownController,但ListView中渲染了3个计时器,所有计时器共用同一个控制器,本身逻辑就存在冲突,无法实现多计时器独立控制
具体实现步骤
1. 搭建全局计时器管理类
创建单例模式的计时器管理类,完全独立于页面存在,统一管理所有倒计时任务的状态,核心字段包含每个任务的唯一ID、总时长、启动时间戳、暂停时的剩余时长、运行状态。
import 'dart:async'; import 'package:awesome_notifications/awesome_notifications.dart'; class TimerTask { final String id; final int totalSeconds; int? startTimeStamp; int remainingSeconds; bool isRunning; TimerTask({ required this.id, required this.totalSeconds, this.remainingSeconds = 0, this.isRunning = false, this.startTimeStamp, }); } class TimerManager { TimerManager._internal(); static final TimerManager _instance = TimerManager._internal(); factory TimerManager() => _instance; final Map<String, TimerTask> _timerTasks = {}; Timer? _globalTickTimer; final StreamController<TimerTask> _timerUpdateController = StreamController<TimerTask>.broadcast(); Stream<TimerTask> get timerUpdateStream => _timerUpdateController.stream; // 新增/启动计时器 void startTimer(String taskId, int totalSeconds) { if (_timerTasks.containsKey(taskId)) { final task = _timerTasks[taskId]!; task.startTimeStamp = DateTime.now().millisecondsSinceEpoch; task.isRunning = true; } else { _timerTasks[taskId] = TimerTask( id: taskId, totalSeconds: totalSeconds, remainingSeconds: totalSeconds, isRunning: true, startTimeStamp: DateTime.now().millisecondsSinceEpoch, ); } _startGlobalTick(); } // 全局秒级定时器,统一更新所有运行中的计时器 void _startGlobalTick() { if (_globalTickTimer != null && _globalTickTimer!.isActive) return; _globalTickTimer = Timer.periodic(const Duration(seconds: 1), (timer) { final now = DateTime.now().millisecondsSinceEpoch; List<String> finishedTasks = []; _timerTasks.forEach((taskId, task) { if (!task.isRunning) return; final elapsed = (now - task.startTimeStamp!) ~/ 1000; task.remainingSeconds = task.totalSeconds - elapsed; if (task.remainingSeconds <= 0) { task.remainingSeconds = 0; task.isRunning = false; finishedTasks.add(taskId); // 触发完成回调,发通知 _triggerTaskFinish(task); } _timerUpdateController.add(task); }); // 移除已完成的任务 for (var id in finishedTasks) { _timerTasks.remove(id); } // 没有运行中的任务就停止全局定时器 if (_timerTasks.values.where((t) => t.isRunning).isEmpty) { _globalTickTimer?.cancel(); _globalTickTimer = null; } }); } // 暂停计时器 void pauseTimer(String taskId) { if (!_timerTasks.containsKey(taskId)) return; final task = _timerTasks[taskId]!; task.isRunning = false; } // 获取指定任务剩余时间 int getRemainingSeconds(String taskId) { return _timerTasks[taskId]?.remainingSeconds ?? 0; } // 触发完成通知 void _triggerTaskFinish(TimerTask task) async { await AwesomeNotifications().createNotification( content: NotificationContent( id: task.id.hashCode, channelKey: 'key1', title: '计时结束', body: '任务${task.id}计时已完成', ) ); } }
2. 配置后台运行权限
要保证应用退到后台、页面销毁后计时器还能运行,需要引入后台服务插件:
- 在
pubspec.yaml中添加依赖:flutter_background_service: ^5.0.5 - 按照插件官方要求配置Android、iOS的后台运行权限,将全局计时器的逻辑移到后台服务中运行,即可保证应用退到后台也不会被系统回收
3. 修改页面渲染逻辑
去掉原来共用的CountDownController,改为监听全局TimerManager的流更新,每个计时器对应独立的任务ID,实时渲染剩余时间和环形进度:
@override void initState() { super.initState(); fetchdata(); // 监听计时器更新,刷新UI TimerManager().timerUpdateStream.listen((task) { if(mounted) setState(() {}); }); } // ListView中每个计时器的渲染逻辑,替换原来的CircularCountDownTimer @override Widget build(BuildContext context) { double width = MediaQuery.of(context).size.width * 0.6; return Scaffold( appBar: AppBar( title: const Text( "Promoters Detail", style: TextStyle(color: Colors.white), ), actions: <Widget>[ IconButton( icon: const Icon( Icons.notifications_active, color: Colors.white, ), onPressed: () => Notify(), ) ], elevation: 5, backgroundColor: Colors.green, ), body: ListView.builder( itemCount: listResponse?.length ?? 0, itemBuilder: (context, index) { String taskId = listResponse[index]['id'].toString(); int totalSeconds = 120; int remaining = TimerManager().getRemainingSeconds(taskId); double progress = totalSeconds == 0 ? 0 : remaining / totalSeconds; return GestureDetector( onTap: () { p_id = listResponse[index]['id'].toString(); p_name = listResponse[index]['name'].toString(); showDialogFunc(context, imgList[index], p_name, descList[index], timeList[index], p_id, a_id); }, child: Card( color: Colors.white, elevation: 5, child: Column( children: [ // 自定义环形倒计时,无需依赖第三方控件 SizedBox( width: MediaQuery.of(context).size.width /6, height: MediaQuery.of(context).size.width /6, child: Stack( alignment: Alignment.center, children: [ CircularProgressIndicator( value: progress, backgroundColor: Colors.white, color: Colors.green, strokeWidth: 10, ), Text( '$remaining', style: const TextStyle(fontSize: 20, color: Colors.black), ) ], ), ), TextButton( onPressed: () => TimerManager().startTimer(taskId, totalSeconds), child: const Text('启动计时'), ), // 原有列表项布局逻辑保持不变 Row( children: <Widget>[ CircleAvatar( radius: 60, backgroundImage: AssetImage(imgList[index]), ), Padding( padding: const EdgeInsets.all(10.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ Text( listResponse[index]['name'].toString(), style: TextStyle( color: Colors.grey.shade500, fontSize: 20.0, fontWeight: FontWeight.w400, ), ), const SizedBox(height: 10), // 原有描述类布局保持不变 Container( width: width, child: Row( children: [ Icon( Icons.add_task_sharp, color: Colors.grey[500], size: 20.0, ), const SizedBox(width: 7), Text( descList[index], maxLines: 3, style: TextStyle( fontSize: 15, color: Colors.grey[500], fontWeight: FontWeight.w500), ), ], ), ), // 剩余原有布局逻辑保持不变 ], ), ) ], ), ], ), ), ); }, ), ); }
注意事项
- 如果你需要应用被彻底杀掉后重启还能恢复之前的计时,可以将
TimerTask的状态持久化到本地存储(比如shared_preferences),应用启动时读取恢复即可 - 多个计时器的通知ID要唯一,避免覆盖,上面示例中用
task.id.hashCode作为通知ID即可 - Android平台要申请锁屏唤醒、后台运行权限,避免系统自动杀死后台进程
内容的提问来源于stack exchange,提问作者ARPIT JAISWAL IET Student
相关产品推荐
相关产品推荐

