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

Flutter:用StatefulBuilder解决Timer刷新致Pinput重置问题

Flutter倒计时导致验证码输入框重置的解决方案

问题描述

我在Flutter应用中实现了倒计时功能,用Text组件显示剩余秒数:

Text(
  "$_start second",
  style: GoogleFonts.nunitoSans(
  color: Colors.amber,
  fontWeight: FontWeight.bold,
  fontSize: 20.0.sp),
  textAlign: TextAlign.center,
),

通过startTimer函数触发倒计时:

void startTimer() {
  const oneSec = Duration(seconds: 1);
  _timer = Timer.periodic(
    oneSec,
    (Timer timer) {
      if (_start == 0) {
        setState(() {
          timer.cancel();
          _isButtonDisabled = false;
        });
      } else {
        setState(() {
          _start--;
        });
      }
    },
  );
}

问题是页面中的Pinput验证码输入框,在启动startTimer后会被刷新重置,无法正常输入。我知道StatefulBuilder可以实现局部状态更新,但我的setState在startTimer函数里,不知道怎么结合使用,求解决方案。

问题根源分析

核心问题有两个:

  1. 控制器和焦点节点在build方法内创建:每次调用setState触发页面重建时,pinController和focusNode都会被重新实例化,直接导致Pinput的输入状态丢失。
  2. 全局setState触发全页面重建:倒计时的setState会刷新整个页面,即使只有倒计时文本和按钮状态需要更新。

分步解决方案

1. 修复控制器和焦点节点的生命周期

把pinController和focusNode从build方法中移出,作为State类的成员变量,确保它们只在组件初始化时创建一次,不会随页面重建而重置:

class _SignUpVerifyClientNumberState extends State<SignUpVerifyClientNumber> {
  late int otpCode;
  late Timer _timer;
  int _start = 150;
  late bool _isButtonDisabled;
  // 移至State类作为成员变量
  late TextEditingController pinController;
  late FocusNode focusNode;

  @override
  void initState() {
    _isButtonDisabled = false;
    // 初始化控制器和焦点节点
    pinController = TextEditingController();
    focusNode = FocusNode();
    super.initState();
  }

  @override
  void dispose() {
    // 销毁资源避免内存泄漏
    pinController.dispose();
    focusNode.dispose();
    _timer.cancel();
    super.dispose();
  }
  // ... 其他代码不变
}

2. 用StatefulBuilder实现局部状态更新

用StatefulBuilder包裹倒计时文本和重发按钮,让setState只刷新这个局部区域,避免影响Pinput组件:

// 替换原页面中倒计时文本和按钮的代码
Gap(10.h),
StatefulBuilder(
  builder: (context, setStateLocal) {
    return Column(
      children: [
        Text(
          "$_start second",
          style: GoogleFonts.nunitoSans(
              color: Colors.amber,
              fontWeight: FontWeight.bold,
              fontSize: 20.0.sp),
          textAlign: TextAlign.center,
        ),
        Gap(10.h),
        SizedBox(
          width: MediaQuery.of(context).size.width / 2,
          height: 40.h,
          child: ElevatedButton(
            style: ElevatedButton.styleFrom(
              backgroundColor: firstAppColor,
              shape: RoundedRectangleBorder(
                borderRadius: BorderRadius.circular(10.sp),
              ),
            ),
            onPressed: _isButtonDisabled ? null : sendOtp,
            child: Text(
              resendNewCodeString,
              style: TextStyle(
                  color: Colors.white,
                  fontFamily: GoogleFonts.openSans().fontFamily,
                  fontWeight: FontWeight.w600,
                  fontSize: 13.0.sp),
            ),
          ),
        ),
      ],
    );
  },
),

3. 可选:拆分独立组件(更优雅的方案)

把倒计时和重发按钮拆成独立的StatefulWidget,完全隔离状态管理,避免与主页面的状态互相影响:

class CountdownResendButton extends StatefulWidget {
  final bool isDisabled;
  final VoidCallback onResend;
  final int initialSeconds;
  const CountdownResendButton({
    super.key,
    required this.isDisabled,
    required this.onResend,
    this.initialSeconds = 150,
  });

  @override
  State<CountdownResendButton> createState() => _CountdownResendButtonState();
}

class _CountdownResendButtonState extends State<CountdownResendButton> {
  late Timer _timer;
  late int _start;
  late bool _isButtonDisabled;

  @override
  void initState() {
    _isButtonDisabled = widget.isDisabled;
    _start = widget.initialSeconds;
    if (_isButtonDisabled) startTimer();
    super.initState();
  }

  @override
  void didUpdateWidget(covariant CountdownResendButton oldWidget) {
    if (oldWidget.isDisabled != widget.isDisabled && widget.isDisabled) {
      _start = widget.initialSeconds;
      startTimer();
    }
    super.didUpdateWidget(oldWidget);
  }

  void startTimer() {
    const oneSec = Duration(seconds: 1);
    _timer = Timer.periodic(
      oneSec,
      (Timer timer) {
        setState(() {
          if (_start == 0) {
            timer.cancel();
            _isButtonDisabled = false;
          } else {
            _start--;
          }
        });
      },
    );
  }

  @override
  void dispose() {
    _timer.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text(
          "$_start second",
          style: GoogleFonts.nunitoSans(
              color: Colors.amber,
              fontWeight: FontWeight.bold,
              fontSize: 20.0.sp),
          textAlign: TextAlign.center,
        ),
        Gap(10.h),
        SizedBox(
          width: MediaQuery.of(context).size.width / 2,
          height: 40.h,
          child: ElevatedButton(
            style: ElevatedButton.styleFrom(
              backgroundColor: firstAppColor,
              shape: RoundedRectangleBorder(
                borderRadius: BorderRadius.circular(10.sp),
              ),
            ),
            onPressed: _isButtonDisabled ? null : widget.onResend,
            child: Text(
              resendNewCodeString,
              style: TextStyle(
                  color: Colors.white,
                  fontFamily: GoogleFonts.openSans().fontFamily,
                  fontWeight: FontWeight.w600,
                  fontSize: 13.0.sp),
            ),
          ),
        ),
      ],
    );
  }
}

然后在原页面中调用这个组件:

Gap(10.h),
CountdownResendButton(
  isDisabled: _isButtonDisabled,
  onResend: sendOtp,
),

最终效果

完成以上修改后,倒计时的状态更新只会影响自身区域,Pinput的输入状态不会再被重置,同时页面性能也得到了优化。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.20 14:37:04