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

Flutter中如何保持TabBar内组件状态,让计时器后台运行?

问题描述

我是Flutter新手,目前遇到一个问题:我开发的应用包含StateTimerPage组件,该组件通过TabBar承载多个计时器页面。希望离开StateTimerPage时,TabBar内的计时器组件状态能被保留,即计时器在用户切换到其他组件时仍能继续运行。我尝试给StateTimerPage和计时器组件都添加AutomaticKeepAliveClientMixin,但并未解决问题,相关代码如下:

计时器通用组件

import 'package:cgk/timer1.dart';
import 'package:cgk/timer2.dart';
import 'package:cgk/timer3.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';

class StateTimerPage extends StatefulWidget {
  const StateTimerPage({Key? key}) : super(key: key);

  @override
  _StateTimerPageState createState() => _StateTimerPageState();
}

class _StateTimerPageState extends State<StateTimerPage> with AutomaticKeepAliveClientMixin {

  @override
  bool get wantKeepAlive => true;

  @override
  Widget build(BuildContext context) {
    super.build(context);
    final size = MediaQuery
        .of(context)
        .size;
    return Center(
        child: DefaultTabController(
            length: 3,
            child: Scaffold(
              appBar: AppBar(
                backgroundColor: const Color(0xff4397de),
                centerTitle: true,
                title: const Text("Таймеры"),
                automaticallyImplyLeading: false,
                bottom: TabBar(
                    indicatorColor: const Color(0xff418ecd),
                    padding: EdgeInsets.all(1),
                    tabs: [
                      Tab(
                        icon: SvgPicture.asset(
                            'assets/Group 1.svg',
                            width: 25.0,
                            height: 25.0,
                        ),
                      ),
                      Tab(
                          icon: SvgPicture.asset(
                              'assets/Group 2.svg',
                              width: 25.0,
                              height: 25.0,
                          )
                      ),
                      Tab(
                          icon: SvgPicture.asset(
                              'assets/Group 3.svg',
                              width: 25.0,
                              height: 25.0,
                          )
                      )
                    ]
                ),
              ),
              body: TabBarView(
                children: [
                  StateTimer1Page(),
                  StateTimer2Page(),
                  StateTimer3Page(),
                ],
              ),
            )
        )
    );
  }
}

计时器示例组件

import 'dart:async';
import 'package:cgk/menu.dart';
import 'package:flutter/material.dart';
import 'package:audioplayers/audioplayers.dart';
import 'package:flutter_svg/flutter_svg.dart';

class StateTimer1Page extends StatefulWidget {
  const StateTimer1Page({Key? key}) : super(key: key);

  @override
  _StateTimer1PageState createState() => _StateTimer1PageState();
}

// Класс Таймера
class _StateTimer1PageState extends State<StateTimer1Page> with WidgetsBindingObserver/*, AutomaticKeepAliveClientMixin*/{
  Timer? _timer;
  late int _waitTime;
  var _percent = 1.0;
  var isStart = false;
  var timeStr = '05:00';

  //@override
  //bool get wantKeepAlive => true;

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
    _waitTime = 60;
    _calculateTime();
  }

  @override
  void dispose() {
    super.dispose();
    WidgetsBinding.instance.removeObserver(this);
    _timer?.cancel();
  }

  // Функция кнопки Старт
  void start(BuildContext context) {
    if (_waitTime > 0) {
      setState(() {
        isStart = true;
      });
      _timer = Timer.periodic(const Duration(seconds: 1), (timer) {
        _waitTime -= 1;
        _calculateTime();
        if (_waitTime <= 0) {
          ScaffoldMessenger.of(context)
              .showSnackBar(const SnackBar(content: Text('Finished')));
          sound! ? AudioPlayer().play(AssetSource('startTimer.mp3')) : null;
          pause();
        }
      });
    }
  }

  // Функция кнопки Перезагрузки
  void restart() {
    _waitTime = 60;
    _calculateTime();
  }

  // Функция кнопки Паузы
  void pause() {
    _timer?.cancel();
    setState(
          () {
        isStart = false;
      },
    );
  }

  // Дополнительная функция для вычисления времени
  void _calculateTime() {
    var minuteStr = (_waitTime ~/ 60).toString().padLeft(2, '0');
    var secondStr = (_waitTime % 60).toString().padLeft(2, '0');
    setState(() {
      _percent = _waitTime / 60;
      timeStr = '$minuteStr:$secondStr';
    });
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    if (state == AppLifecycleState.paused) pause();
  }

  @override
  Widget build(BuildContext context) {
    //super.build(context);
    final size = MediaQuery.of(context).size;
    return Scaffold(
      backgroundColor: const Color(0xff3987c8),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Stack(
              alignment: Alignment.center,
              children: [
                Container(
                  height: size.height * 0.3,
                  width: size.height * 0.3,
                  margin: const EdgeInsets.all(10),
                  child: CircularProgressIndicator(
                    value: _percent,
                    backgroundColor: Colors.red,
                    strokeWidth: 11,
                    color: const Color(0xff235d8c),
                  ),
                ),
                Positioned(
                  child: Text(
                    timeStr,
                    style: const TextStyle(
                      fontWeight: FontWeight.bold,
                      fontSize: 50,
                    ),
                    textAlign: TextAlign.center,
                  ),
                ),
                Container(
                    height: size.height * 0.35,
                    width: size.height * 0.35,
                    alignment: Alignment.topCenter,
                  child: SvgPicture.asset(
                    'assets/Group 6.svg',
                    alignment: Alignment.topCenter,
                    width: size.width * 0.05,
                    height: size.height * 0.05,
                  ),
                )
              ],
            ),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Container(
                  height: size.height * 0.15,
                  width: size.height * 0.15,
                  margin: const EdgeInsets.all(20),
                  child: FloatingActionButton(
                      heroTag: "btn1",
                      backgroundColor: const Color(0xff418ecd),
                      onPressed: () {
                        isStart ? pause() : start(context);
                      },
                      child: isStart
                          ? const Icon(
                        Icons.pause,
                        size: 80,
                        color: Color(0xff235d8c),
                      )
                          : const Icon(
                        Icons.play_arrow_outlined,
                        size: 80,
                        color: Color(0xff235d8c),
                      )),
                ),
                Container(
                  height: size.height * 0.15,
                  width: size.height * 0.15,
                  margin: const EdgeInsets.all(20),
                  child: FloatingActionButton(
                      heroTag: "btn2",
                      backgroundColor: const Color(0xff418ecd),
                      onPressed: () {
                        restart();
                      },
                      child: const Icon(
                        Icons.restart_alt,
                        size: 80,
                        color: Color(0xff235d8c),
                      )),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}
解决方案

你遇到的核心问题是:AutomaticKeepAliveClientMixin只能保证TabBar内部切换时页面不被销毁,但当你完全离开StateTimerPage(比如跳转到其他页面)时,整个组件树都会被销毁,内部计时器自然停止。要实现离开页面后计时器仍运行,必须把计时器逻辑从组件中抽离,用独立的全局服务来维护状态。

步骤1:创建全局计时器服务类

用单例模式实现一个独立的计时器服务,让它脱离组件生命周期,即使组件销毁,服务依然能在后台运行:

import 'dart:async';

class TimerService {
  static final TimerService _instance = TimerService._internal();
  factory TimerService() => _instance;
  TimerService._internal();

  Timer? _timer;
  int _waitTime = 60;
  bool isStart = false;
  String timeStr = '01:00';
  double _percent = 1.0;

  // 状态更新回调,通知UI刷新
  Function()? onUpdate;

  void start() {
    if (_waitTime > 0 && !isStart) {
      isStart = true;
      _timer = Timer.periodic(const Duration(seconds: 1), (timer) {
        _waitTime -= 1;
        _calculateTime();
        if (_waitTime <= 0) {
          pause();
          onUpdate?.call();
        }
      });
      onUpdate?.call();
    }
  }

  void pause() {
    _timer?.cancel();
    isStart = false;
    onUpdate?.call();
  }

  void restart() {
    _waitTime = 60;
    _calculateTime();
    onUpdate?.call();
  }

  void _calculateTime() {
    var minuteStr = (_waitTime ~/ 60).toString().padLeft(2, '0');
    var secondStr = (_waitTime % 60).toString().padLeft(2, '0');
    _percent = _waitTime / 60;
    timeStr = '$minuteStr:$secondStr';
    onUpdate?.call();
  }

  double get percent => _percent;

  // 全局退出时再销毁计时器
  void dispose() {
    _timer?.cancel();
  }
}

步骤2:修改计时器组件,依赖全局服务

把组件内部的计时器状态替换为调用全局服务,组件只负责UI渲染和响应服务的状态变化:

import 'dart:async';
import 'package:cgk/menu.dart';
import 'package:flutter/material.dart';
import 'package:audioplayers/audioplayers.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'timer_service.dart'; // 导入服务类

class StateTimer1Page extends StatefulWidget {
  const StateTimer1Page({Key? key}) : super(key: key);

  @override
  _StateTimer1PageState createState() => _StateTimer1PageState();
}

class _StateTimer1PageState extends State<StateTimer1Page> with WidgetsBindingObserver, AutomaticKeepAliveClientMixin {
  final TimerService _timerService = TimerService();

  @override
  bool get wantKeepAlive => true;

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
    // 监听服务状态变化,触发UI刷新
    _timerService.onUpdate = () {
      if (mounted) setState(() {});
    };
  }

  @override
  void dispose() {
    super.dispose();
    WidgetsBinding.instance.removeObserver(this);
    // 不要在这里销毁服务,其他页面可能还在使用
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    if (state == AppLifecycleState.paused) _timerService.pause();
  }

  @override
  Widget build(BuildContext context) {
    super.build(context);
    final size = MediaQuery.of(context).size;
    return Scaffold(
      backgroundColor: const Color(0xff3987c8),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Stack(
              alignment: Alignment.center,
              children: [
                Container(
                  height: size.height * 0.3,
                  width: size.height * 0.3,
                  margin: const EdgeInsets.all(10),
                  child: CircularProgressIndicator(
                    value: _timerService.percent,
                    backgroundColor: Colors.red,
                    strokeWidth: 11,
                    color: const Color(0xff235d8c),
                  ),
                ),
                Positioned(
                  child: Text(
                    _timerService.timeStr,
                    style: const TextStyle(
                      fontWeight: FontWeight.bold,
                      fontSize: 50,
                    ),
                    textAlign: TextAlign.center,
                  ),
                ),
                Container(
                    height: size.height * 0.35,
                    width: size.height * 0.35,
                    alignment: Alignment.topCenter,
                  child: SvgPicture.asset(
                    'assets/Group 6.svg',
                    alignment: Alignment.topCenter,
                    width: size.width * 0.05,
                    height: size.height * 0.05,
                  ),
                )
              ],
            ),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Container(
                  height: size.height * 0.15,
                  width: size.height * 0.15,
                  margin: const EdgeInsets.all(20),
                  child: FloatingActionButton(
                      heroTag: "btn1",
                      backgroundColor: const Color(0xff418ecd),
                      onPressed: () {
                        _timerService.isStart ? _timerService.pause() : _timerService.start();
                      },
                      child: _timerService.isStart
                          ? const Icon(
                        Icons.pause,
                        size: 80,
                        color: Color(0xff235d8c),
                      )
                          : const Icon(
                        Icons.play_arrow_outlined,
                        size: 80,
                        color: Color(0xff235d8c),
                      )),
                ),
                Container(
                  height: size.height * 0.15,
                  width: size.height * 0.15,
                  margin: const EdgeInsets.all(20),
                  child: FloatingActionButton(
                      heroTag: "btn2",
                      backgroundColor: const Color(0xff418ecd),
                      onPressed: () {
                        _timerService.restart();
                      },
                      child: const Icon(
                        Icons.restart_alt,
                        size: 80,
                        color: Color(0xff235d8c),
                      )),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

关键说明

  1. AutomaticKeepAliveClientMixin的作用仅限TabBar内部页面切换,离开整个StateTimerPage时,组件还是会被销毁,所以必须用全局服务保存状态。
  2. 全局单例服务不受组件生命周期影响,即使页面销毁,计时器依然在后台运行,回到页面时能直接获取最新状态。
  3. 如果需要三个Tab各对应独立的计时器,可以修改服务类支持多实例,或者创建三个不同的服务类。

内容的提问来源于stack exchange,提问作者Usernamedrew

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.23 17:17:12