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), )), ), ], ), ], ), ), ); } }
关键说明
AutomaticKeepAliveClientMixin的作用仅限TabBar内部页面切换,离开整个StateTimerPage时,组件还是会被销毁,所以必须用全局服务保存状态。- 全局单例服务不受组件生命周期影响,即使页面销毁,计时器依然在后台运行,回到页面时能直接获取最新状态。
- 如果需要三个Tab各对应独立的计时器,可以修改服务类支持多实例,或者创建三个不同的服务类。
内容的提问来源于stack exchange,提问作者Usernamedrew
相关产品推荐
相关产品推荐

