Flutter组件封装:验证码倒计时按钮 TimerButton

一、需求来源

发现问题:

最近发现老代码中验证码倒计时按钮在低配机器上会出现数字乱闪的情况,查询资料之后发现是 jank 导致的问题。

解决办法:

思路是验证码接口请求成功之后,立即计算当前时间未来60s的目标时间点。定时器每秒轮训一次,查询当前时间距离目标时间点的剩余秒,然后显示在界面上。 通过mixin 做了倒计时逻辑抽离,方便二次封装组件。现成组件是 TimerButton,效果如下:

二、使用示例

dart 复制代码
TimerButton(
  onRequest: () async {
    await Future.delayed(Duration(milliseconds: 1));//假装在请求
    return true;
  },
),

三、源码

1、倒计时混入mixin: CountDownTimer,倒计时逻辑抽离

dart 复制代码
import 'dart:async';

import 'package:flutter/material.dart';
import 'package:flutter_templet_project/extension/extension_local.dart';

/// 倒计时
mixin CountDownTimer<T extends StatefulWidget> on State<T>, WidgetsBindingObserver {
  DateTime? _endTime;
  Timer? _timer;

  int get limitSecond => 60;

  final isCountingDownVN = ValueNotifier(false);
  late final countdownVN = ValueNotifier(limitSecond);

  @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    _timer?.cancel();
    super.dispose();
  }

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
  }

  // 处理 app 暂停/恢复(确保从后台回来能立即刷新)
  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    if (state == AppLifecycleState.resumed) {
      // app 回到前台,立刻重算一次
      _updateRemaining();
    }
  }

  /// 开始倒计时
  void startCountdown() {
    isCountingDownVN.value = true;
    countdownVN.value = limitSecond;

    _endTime = DateTime.now().add(Duration(seconds: limitSecond));
    _updateRemaining(); // 立即计算一次(避免 UI 延迟)
    _timer?.cancel();
    // 周期短一点以保证恢复后能尽快反映(但 setState 只有在秒数变化才触发)
    _timer = Timer.periodic(const Duration(seconds: 1), (_) => _updateRemaining());
  }

  void _updateRemaining() {
    if (_endTime == null) {
      return;
    }
    final secondsLeft = _endTime!.difference(DateTime.now()).inSeconds.clamp(0, limitSecond);
    DLog.d(["secondsLeft: $secondsLeft"]);
    if (secondsLeft <= 0) {
      isCountingDownVN.value = false;
      _timer?.cancel();
      _endTime = null;
    } else {
      countdownVN.value = secondsLeft;
    }
  }
}

2、TimerButton源码

dart 复制代码
// 验证码
class TimerButton extends StatefulWidget {
  const TimerButton({
    super.key,
    required this.onRequest,
  });

  final Future<bool> Function() onRequest;

  @override
  State<TimerButton> createState() => TimerButtonState();
}

/// 倒计时
class TimerButtonState extends State<TimerButton> with WidgetsBindingObserver, CountDownTimer {
  @override
  Widget build(BuildContext context) {
    return ValueListenableBuilder(
      valueListenable: isCountingDownVN,
      builder: (context, isCountingDown, child) {
        // final disabledBackgroundColor = themeProvider.isDark ? const Color(0xFF3A3A48) : const Color(0xFFDEDEDE);
        // final disabledForegroundColor = themeProvider.isDark ? const Color(0xFF7C7C85) : const Color(0xFFA7A7AE);

        return ElevatedButton(
          style: ElevatedButton.styleFrom(
            minimumSize: Size(60, 30),
            maximumSize: Size(100, 30),
            padding: EdgeInsets.symmetric(horizontal: 10, vertical: 4),
            tapTargetSize: MaterialTapTargetSize.shrinkWrap,
            backgroundColor: Colors.red,
            // disabledBackgroundColor: disabledBackgroundColor,
            foregroundColor: Colors.white,
            disabledForegroundColor: Colors.white,
            shape: RoundedRectangleBorder(
              borderRadius: BorderRadius.circular(4), // 设置圆角半径
            ),
            elevation: 0,
          ),
          onPressed: isCountingDown
              ? null
              : () async {
                  var res = await widget.onRequest();
                  if (res) {
                    startCountdown();
                  }
                },
          child: ValueListenableBuilder(
            valueListenable: countdownVN,
            builder: (context, value, child) {
              return Container(
                alignment: Alignment.center,
                child: Text(
                  isCountingDown ? '$value秒后重试' : '发送验证码',
                ),
              );
            },
          ),
        );
      },
    );
  }
}

最后、总结

倒计时逻辑实现起来并不复杂,但也算开发中遇到的疑难杂(高配机器永远不出现,低配经常出现),顺手做个记录。

github

相关推荐
一只大侠的侠3 小时前
Flutter开源鸿蒙跨平台训练营 Day7Flutter+ArkTS双方案实现轮播图+搜索框+导航组件
flutter·开源·harmonyos
五月君_3 小时前
炸裂!Claude Opus 4.6 与 GPT-5.3 同日发布:前端人的“自动驾驶“时刻到了?
前端·gpt
Mr Xu_3 小时前
前端开发中CSS代码的优化与复用:从公共样式提取到CSS变量的最佳实践
前端·css
一只大侠的侠4 小时前
Flutter开源鸿蒙跨平台训练营 Day9分类数据的获取与渲染实现
flutter·开源·harmonyos
鹏北海-RemHusband4 小时前
从零到一:基于 micro-app 的企业级微前端模板完整实现指南
前端·微服务·架构
LYFlied4 小时前
AI大时代下前端跨端解决方案的现状与演进路径
前端·人工智能
光影少年4 小时前
AI 前端 / 高级前端
前端·人工智能·状态模式
一位搞嵌入式的 genius4 小时前
深入 JavaScript 函数式编程:从基础到实战(含面试题解析)
前端·javascript·函数式
anOnion4 小时前
构建无障碍组件之Alert Dialog Pattern
前端·html·交互设计
一只大侠的侠4 小时前
Flutter开源鸿蒙跨平台训练营 Day 5Flutter开发鸿蒙电商应用
flutter·开源·harmonyos