You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter实现多任务倒计时器后台持续运行方案求助

问题根源

你当前使用的CountDownController实例是绑定在当前页面的State中的,按下返回键后页面被销毁,State和持有的控制器、定时器实例都会被系统回收,倒计时自然就停止运行了。

实现方案

要实现和页面生命周期解绑的持久倒计时,你需要把定时器的逻辑从页面层抽离,通过全局/跨页面的状态持有者管理任务,再配合前台服务保活,具体步骤如下:

  • 第一步:抽象倒计时任务模型,统一管理每个任务的ID、总时长、剩余时长、运行状态、完成回调
  • 第二步:创建全局的定时器管理类,独立于页面生命周期运行,统一调度所有倒计时任务
  • 第三步:若需要应用退到后台也不被系统杀死,需配置原生端前台服务权限
  • 第四步:使用本地通知插件实现计时结束的提醒能力
示例代码

首先在pubspec.yaml中添加所需依赖:

dependencies:
  flutter:
    sdk: flutter
  circular_countdown_timer: ^0.2.3
  provider: ^6.1.1
  flutter_local_notifications: ^16.1.0

1. 倒计时任务模型与全局管理类

import 'package:flutter/foundation.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';

// 倒计时任务模型
class CountDownTask {
  final String taskId;
  final int totalDuration;
  int remainingDuration;
  bool isRunning;
  final Function? onComplete;

  CountDownTask({
    required this.taskId,
    required this.totalDuration,
    required this.remainingDuration,
    this.isRunning = false,
    this.onComplete,
  });
}

// 全局倒计时管理类
class CountDownManager extends ChangeNotifier {
  final Map<String, CountDownTask> tasks = {};
  final FlutterLocalNotificationsPlugin _notificationsPlugin = FlutterLocalNotificationsPlugin();

  // 单例实例
  static final CountDownManager _instance = CountDownManager._internal();
  factory CountDownManager() => _instance;
  CountDownManager._internal() {
    _initNotifications();
  }

  // 初始化通知
  void _initNotifications() async {
    const AndroidInitializationSettings initializationSettingsAndroid = AndroidInitializationSettings('@mipmap/ic_launcher');
    const InitializationSettings initializationSettings = InitializationSettings(android: initializationSettingsAndroid);
    await _notificationsPlugin.initialize(initializationSettings);
  }

  // 添加新倒计时任务
  void addTask(CountDownTask task) {
    tasks[task.taskId] = task;
    if (task.isRunning) startTaskTimer(task.taskId);
    notifyListeners();
  }

  // 启动任务倒计时
  void startTaskTimer(String taskId) {
    final task = tasks[taskId];
    if (task == null || !task.isRunning) return;

    Future.doWhile(() async {
      await Future.delayed(const Duration(seconds: 1));
      if (!tasks.containsKey(taskId) || !task.isRunning || task.remainingDuration <= 0) {
        if (task.remainingDuration <= 0) {
          task.onComplete?.call();
          _sendCompleteNotification(taskId);
        }
        return false;
      }
      task.remainingDuration--;
      notifyListeners();
      return true;
    });
  }

  // 发送完成通知
  void _sendCompleteNotification(String taskId) async {
    const AndroidNotificationDetails androidDetails = AndroidNotificationDetails(
      'countdown_channel',
      '倒计时提醒',
      importance: Importance.max,
      priority: Priority.high,
    );
    const NotificationDetails notificationDetails = NotificationDetails(android: androidDetails);
    await _notificationsPlugin.show(
      taskId.hashCode,
      '倒计时结束',
      '您的任务倒计时已完成',
      notificationDetails,
    );
  }

  // 获取指定任务剩余时长
  int getRemainingDuration(String taskId) => tasks[taskId]?.remainingDuration ?? 0;
}

2. 页面中使用改造后的倒计时

import 'package:circular_countdown_timer/circular_count_down_timer.dart';
import 'package:flutter/material.dart';

class CountDownPage extends StatefulWidget {
  const CountDownPage({super.key});

  @override
  State<CountDownPage> createState() => _CountDownPageState();
}

class _CountDownPageState extends State<CountDownPage> {
  final CountDownController _controller = CountDownController();
  final String _taskId = "task_001"; // 每个任务对应唯一ID
  final CountDownManager _manager = CountDownManager();

  @override
  void initState() {
    super.initState();
    // 初始化任务,如果之前已经启动过,直接读取剩余时长
    if (_manager.getRemainingDuration(_taskId) <= 0) {
      _manager.addTask(CountDownTask(
        taskId: _taskId,
        totalDuration: 120,
        remainingDuration: 120,
        isRunning: false,
      ));
    }
    // 监听全局管理器的状态更新,同步到页面的倒计时控制器
    _manager.addListener(() {
      if (mounted) setState(() {});
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("多任务倒计时")),
      body: Center(
        child: CircularCountDownTimer(
          width: MediaQuery.of(context).size.width / 6,
          height: MediaQuery.of(context).size.width / 6,
          duration: _manager.getRemainingDuration(_taskId),
          initialDuration: 0,
          fillColor: Colors.green,
          ringColor: Colors.white,
          controller: _controller,
          backgroundColor: Colors.white54,
          strokeWidth: 10.0,
          strokeCap: StrokeCap.round,
          isTimerTextShown: true,
          isReverse: false,
          onComplete: () {
            // 此处回调仅页面存在时触发,全局回调在管理类中配置
          },
          textStyle: const TextStyle(fontSize: 20.0, color: Colors.black),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          final task = _manager.tasks[_taskId];
          if (task != null) {
            task.isRunning = true;
            _manager.startTaskTimer(_taskId);
            _controller.start();
          }
        },
        child: const Icon(Icons.play_arrow),
      ),
    );
  }

  // 拦截返回键,仅退出页面不销毁倒计时任务
  @override
  Future<bool> didPopRoute() async {
    Navigator.pop(context);
    return true;
  }
}
后台保活补充配置

如果需要应用退到后台后倒计时也不会被系统终止,你需要额外配置:

  • 安卓端:在AndroidManifest.xml中添加前台服务权限,启动倒计时时启动前台服务,在通知栏展示运行中的通知,避免系统回收应用进程
  • iOS端:在Xcode中开启后台模式,勾选Audio, AirPlay, and Picture in Picture和Background fetch权限,实现后台保活

内容的提问来源于stack exchange,提问作者ARPIT JAISWAL IET Student

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.09 00:27:02