Flutter中Timer Button计时时触发setState报错的解决方法
问题原因
当TimerButton处于倒计时状态时,点击Verify按钮完成认证后,若当前页面(或TimerButton所在组件)被从Widget树移除(比如跳转新页面),TimerButton的State会进入defunct状态,但内部计时器仍在运行。计时器触发状态更新时,会调用已dispose的State的setState,从而抛出setState() called after dispose()错误。
解决方案
方案1:通过GlobalKey主动取消TimerButton计时器
如果你使用的是pub.dev上的timer_button包,可以通过GlobalKey获取其State,在认证成功后主动取消计时器:
- 定义GlobalKey:
final GlobalKey<TimerButtonState> _timerButtonKey = GlobalKey<TimerButtonState>();
- 给TimerButton绑定Key:
TimerButton( key: _timerButtonKey, buttonType: ButtonType.TextButton, label: "Resend Code".tr, timeOutInSeconds: 30, onPressed: () async { await sendOTP(); }, resetTimerOnPressed: false, disabledColor: Colors.grey, color: const Color.fromARGB(255, 245, 91, 165), disabledTextStyle: TextStyle(fontSize: 12.sp, color: Colors.white), activeTextStyle: TextStyle(fontSize: 13.sp, color: Colors.white), ),
- 在认证成功后取消计时器:
修改signInWithPhoneAuthCredential方法,当用户认证成功时调用取消方法:
void signInWithPhoneAuthCredential(PhoneAuthCredential phoneAuthCredential) async { setState(() { loading = true; }); try { final authCredential = await _auth.signInWithCredential(phoneAuthCredential); setState(() { loading = false; }); if (authCredential.user != null) { // 取消TimerButton的计时器 _timerButtonKey.currentState?.cancelTimer(); // 执行页面跳转或后续逻辑 } } on FirebaseAuthException catch (e) { setState(() { loading = false; }); Flushbar( message: e.message, duration: const Duration(seconds: 4), ).show(context); } }
方案2:自定义倒计时按钮(完全控制生命周期)
如果第三方包不够灵活,自己实现倒计时按钮可以从根源避免问题,同时确保dispose时自动取消计时器:
class CustomTimerButton extends StatefulWidget { final String label; final int timeoutSeconds; final VoidCallback onPressed; final Color color; final Color disabledColor; final TextStyle activeTextStyle; final TextStyle disabledTextStyle; const CustomTimerButton({ super.key, required this.label, required this.timeoutSeconds, required this.onPressed, required this.color, required this.disabledColor, required this.activeTextStyle, required this.disabledTextStyle, }); @override State<CustomTimerButton> createState() => _CustomTimerButtonState(); } class _CustomTimerButtonState extends State<CustomTimerButton> { late int _remainingSeconds; Timer? _timer; bool _isActive = true; @override void initState() { super.initState(); _remainingSeconds = widget.timeoutSeconds; startTimer(); } void startTimer() { _isActive = false; _timer = Timer.periodic(const Duration(seconds: 1), (timer) { if (_remainingSeconds <= 0) { timer.cancel(); if (mounted) { setState(() { _isActive = true; _remainingSeconds = widget.timeoutSeconds; }); } } else { if (mounted) { setState(() { _remainingSeconds--; }); } } }); } void cancelTimer() { _timer?.cancel(); } @override void dispose() { _timer?.cancel(); super.dispose(); } @override Widget build(BuildContext context) { return TextButton( onPressed: _isActive ? () { widget.onPressed(); startTimer(); } : null, style: TextButton.styleFrom( backgroundColor: _isActive ? widget.color : widget.disabledColor, ), child: Text( _isActive ? widget.label : "$_remainingSeconds 秒后重新发送", style: _isActive ? widget.activeTextStyle : widget.disabledTextStyle, ), ); } }
使用时同样可以通过GlobalKey获取State并调用cancelTimer(),或者它会在组件dispose时自动停止计时器。
额外优化:所有setState前检查mounted状态
为了避免其他场景下的同类错误,在调用setState前都加上mounted检查:
void signInWithPhoneAuthCredential(PhoneAuthCredential phoneAuthCredential) async { if (mounted) { setState(() { loading = true; }); } try { final authCredential = await _auth.signInWithCredential(phoneAuthCredential); if (mounted) { setState(() { loading = false; }); } if (authCredential.user != null) { _timerButtonKey.currentState?.cancelTimer(); // 后续逻辑 } } on FirebaseAuthException catch (e) { if (mounted) { setState(() { loading = false; }); Flushbar( message: e.message, duration: const Duration(seconds: 4), ).show(context); } } }
内容的提问来源于stack exchange,提问作者Mark Nugromentry
相关产品推荐
相关产品推荐

