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

Flutter底部弹窗计时器不实时递减及验证功能异常问题排查

解决Flutter底部弹窗计时器不实时递减的问题

问题描述

Flutter开发中,底部弹窗(bottom sheet)的计时器无法实现每秒实时递减,同时表单验证逻辑也存在错误,尝试过StreamBuilder和StatefulBuilder方案均未解决。

现有代码的核心问题

  1. 计时器逻辑空实现:startTimer函数里的Timer.periodic没有编写任何倒计时逻辑,既没减少duration,也没触发状态更新
  2. 状态更新未触发:即使修改了duration,也没有调用StatefulBuilder的bottomState方法通知UI重建
  3. 表单验证key不匹配:弹窗内的Form用的是_validatorKey,但按钮点击时却调用_formKey的validate,导致验证无效
  4. StreamBuilder无状态关联:当前StreamBuilder只是每秒触发,但没有关联duration的变化,所以UI始终显示初始值

修复步骤

1. 完善计时器核心逻辑

修改startTimer函数,让它接收StatefulBuilder的状态更新回调,每次倒计时都更新状态:

void startTimer(StateSetter bottomState) {
  timer?.cancel();
  // 重置倒计时为3分钟
  duration = Duration(minutes: 3);
  timer = Timer.periodic(Duration(seconds: 1), (timer) {
    bottomState(() {
      if (duration.inSeconds <= 0) {
        timer.cancel(); // 倒计时结束后停止计时器
      } else {
        duration -= Duration(seconds: 1); // 每秒减1秒
      }
    });
  });
}

2. 替换无用的StreamBuilder

既然用StatefulBuilder管理状态,直接渲染当前的duration即可,去掉冗余的StreamBuilder:

// 替换原来的StreamBuilder代码
Text(
  '${duration.inMinutes}:${(duration.inSeconds % 60).toString().padLeft(2, '0')}',
  style: TextStyle(
    fontFamily: 'Pretendard',
    fontSize: 14,
    fontWeight: FontWeight.w300,
  ),
),

3. 修正表单验证逻辑

统一Form的key和按钮验证时使用的key:

// 按钮onPressed代码修改为
onPressed: () {
  final form = _validatorKey.currentState!;
  if (form.validate()) {
    timer?.cancel(); // 关闭弹窗时记得取消计时器
    Navigator.of(context).pop();
    _contractSheet(context);
  } else {
    print('Validation failed.');
  }
},

4. 优化计时器启动时机

避免StatefulBuilder每次重建都重复启动计时器,用addPostFrameCallback确保只在第一次构建时启动:

return StatefulBuilder(builder: (context, StateSetter bottomState) {
  // 延迟到首次布局完成后启动计时器
  WidgetsBinding.instance.addPostFrameCallback((_) {
    startTimer(bottomState);
  });
  // ... 后续的Container布局代码不变
});

完整修改后的弹窗函数示例

Future<dynamic> _validate_modal(BuildContext context) {
  return showModalBottomSheet(
    context: context,
    isScrollControlled: true,
    shape: RoundedRectangleBorder(
      borderRadius: BorderRadius.only(
          topLeft: Radius.circular(30.0), topRight: Radius.circular(30.0)),
    ),
    builder: (context) {
      return StatefulBuilder(builder: (context, StateSetter bottomState) {
        WidgetsBinding.instance.addPostFrameCallback((_) {
          startTimer(bottomState);
        });
        return Container(
          padding: MediaQuery.of(context).viewInsets,
          child: Padding(
            padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 20),
            child: Column(
              mainAxisSize: MainAxisSize.min,
              children: [
                Center(
                  child: Text(
                    '输入验证码',
                    style: TextStyle(
                        fontFamily: 'Pretendard',
                        fontWeight: FontWeight.w500,
                        fontSize: 16),
                  ),
                ),
                Padding(
                  padding: const EdgeInsets.only(top: 20),
                  child: Form(
                    key: _validatorKey,
                    child: TextFormField(
                      controller: _validatorController,
                      validator: (value) {
                        if (value == null || value.isEmpty) {
                          return '请输入验证码';
                        } else if (value.length != 6) {
                          return '请输入6位验证码';
                        }
                        return null;
                      },
                      keyboardType: TextInputType.number,
                      inputFormatters: [
                        FilteringTextInputFormatter.digitsOnly,
                        LengthLimitingTextInputFormatter(6),
                      ],
                      style: TextStyle(
                          fontFamily: 'Pretendard',
                          fontSize: 30,
                          fontWeight: FontWeight.w300),
                      cursorColor: Colors.black,
                      textAlign: TextAlign.center,
                      decoration: InputDecoration(
                        border: InputBorder.none,
                      ),
                    ),
                  ),
                ),
                // 修正后的计时器文本
                Text(
                  '${duration.inMinutes}:${(duration.inSeconds % 60).toString().padLeft(2, '0')}',
                  style: TextStyle(
                    fontFamily: 'Pretendard',
                    fontSize: 14,
                    fontWeight: FontWeight.w300,
                  ),
                ),
                Padding(
                  padding: const EdgeInsets.only(top: 35, bottom: 10),
                  child: Align(
                    alignment: Alignment.bottomCenter,
                    child: Container(
                      width: MediaQuery.of(context).size.width * 1,
                      height: 56,
                      child: FilledButton(
                          style: ButtonStyle(
                              backgroundColor:
                                  MaterialStateProperty.all(Colors.white),
                              shape: MaterialStateProperty.all(
                                  RoundedRectangleBorder(
                                      borderRadius: BorderRadius.circular(0),
                                      side: BorderSide(
                                          color: Colors.black, width: 1.0)))),
                          onPressed: () {
                            final form = _validatorKey.currentState!;
                            if (form.validate()) {
                              timer?.cancel();
                              Navigator.of(context).pop();
                              _contractSheet(context);
                            } else {
                              print('Validation failed.');
                            }
                          },
                          child: Center(
                            child: Text(
                              '다음',
                              style: TextStyle(
                                  fontFamily: 'Pretendard',
                                  fontSize: 16,
                                  fontWeight: FontWeight.w700,
                                  color: Colors.black),
                            ),
                          )),
                    ),
                  ),
                ),
              ],
            ),
          ),
        );
      });
    },
  );
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.26 23:36:03