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

Flutter中Timer Button计时时触发setState报错的解决方法

问题解决:TimerButton计时中点击Verify触发setState报错

问题原因

当TimerButton处于倒计时状态时,点击Verify按钮完成认证后,若当前页面(或TimerButton所在组件)被从Widget树移除(比如跳转新页面),TimerButton的State会进入defunct状态,但内部计时器仍在运行。计时器触发状态更新时,会调用已dispose的State的setState,从而抛出setState() called after dispose()错误。

解决方案

方案1:通过GlobalKey主动取消TimerButton计时器

如果你使用的是pub.dev上的timer_button包,可以通过GlobalKey获取其State,在认证成功后主动取消计时器:

  1. 定义GlobalKey:
final GlobalKey<TimerButtonState> _timerButtonKey = GlobalKey<TimerButtonState>();
  1. 给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),
),
  1. 在认证成功后取消计时器:
    修改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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.05 18:10:35