# 获取验证码

# 需求分析

  • 用户输入手机号,如果输入不正确,无法点击获取验证码;
  • 用于点击获取验证码 -> 进入倒计时
  • 倒计时结束才能再次发送获取验证码

# 初步封装按钮组件

需求&步骤:

  • 创建文件:lib/widgets/base/button/counter_button.dart

  • 把之前写在main.dart中的button进行封装 -> 使用快捷菜单;

  • 创建文件:lib/widgets/base/const/text_const.dart,放置文字相关的常量:

    import 'package:flutter/material.dart';
    
    final TextStyle availableStyle = TextStyle(
      fontSize: 14.0,
      color: Color(0xFF333333),
      fontWeight: FontWeight.w400,
    );
    
    final TextStyle unAvailableStyle = TextStyle(
      fontSize: 14.0,
      color: Colors.black12,
    );
    
  • 给Button加上onPressed回调;

完整代码:

import 'package:flutter/material.dart';
import 'package:my_app/widgets/const/text_const.dart';

class CounterButton extends StatelessWidget {
    final bool active;
  final VoidCallback? onPressed;
  const CounterButton({
    Key? key,
    this.active = true,
    this.onPressed,
  }) : super(key: key);

  
  Widget build(BuildContext context) {
    return ElevatedButton(
      style: ButtonStyle(
        elevation: MaterialStateProperty.all(0),
        backgroundColor: MaterialStateProperty.all(Colors.black12),
        shape: MaterialStateProperty.all(RoundedRectangleBorder(
          borderRadius: BorderRadius.circular(35),
        )),
      ),
      child: Text(
        '获取验证码',
        style: active
            ? availableStyle
            : unAvailableStyle,
      ),
      onPressed: onPressed,
    );
    ;
  }
}

# 倒计时逻辑

需求:

  • 每隔一个duration进行计数;
  • 有一个计时的总数;
  • 需要能够控制计时器的启动与停止;

创建utils/number_count.dart文件,因为倒计时是一个非常通用的逻辑:

// Stream -> 创建计数器
// 每隔1s -> 添加stream
// 定时器 -> 1s -> 计数结束标志 -> count = 60

import 'dart:async';

class NumberCount {
  final int sum;
  final int duration;
  int _count = 1;
  Timer? _timer;

  NumberCount({this.sum = 60, this.duration = 1});

  StreamController<int> _stream = StreamController<int>();
  Stream<int> get stream => _stream.stream;
  // 判断是否可能进行再次进行计数
  bool get avaliable => _count == 1;

  void start() {
    // 计时器的锁,防止重复多次点击
    if (!avaliable) return;
    // 清除历史的Timer,防止多个计时器同时运行
    _timer?.cancel();
    _timer = Timer.periodic(Duration(seconds: duration), (timer) {
      _stream.sink.add(_count);
      _count++;
      if (_count == sum) {
        timer.cancel();
        _count = 1;
      }
    });
  }

  void stop() {
    _timer?.cancel();
  }

  void dispose() {
    _timer?.cancel();
    _stream.close();
  }
}

分析:

  • 创建一个类,定义变量:int sumint duration,定义方法startstopdispose(销毁计数器,回收内存);
  • 定义一个StreamController,用于传递异步的数据,并记录当前的数据,以便视图层刷新;
  • 这里最核心的就是stream的使用方式:Stream<int> get stream => _stream.stream;,定义 一个get方法,相当于暴露出来;

注意:

带下横线的变量为私有变量,这个是dart中的约定。

# 按钮组件逻辑

分析:

  • 状态变化,所以需要从stateless -> stateful组件;
  • 读取Stream数据,使用StreamBuilder
  • 初始化NumberCount,设置数据并读取stream;

使用快捷菜单调整CounterButton为有状态组件:

import 'package:flutter/material.dart';
import 'package:my_app/utils/number_count.dart';
import 'package:my_app/widgets/const/text_const.dart';

class CounterButton extends StatefulWidget {
  final bool active;
  final VoidCallback? onPressed;

  const CounterButton({
    Key? key,
    this.active = true,
    this.onPressed,
  }) : super(key: key);

  
  State<CounterButton> createState() => _CounterButtonState();
}

class _CounterButtonState extends State<CounterButton> {
  String _msg = '获取验证码';
  NumberCount _numberCount = NumberCount();

  
  Widget build(BuildContext context) {
    return StreamBuilder(
        stream: _numberCount.stream,
        builder: (context, snapshot) {
          if (snapshot.hasData) {
            _msg = '已发送${snapshot.data}s';
            if (_numberCount.avaliable) {
              _msg = '重新发送';
            }
          }
          return ElevatedButton(
            style: ButtonStyle(
              elevation: MaterialStateProperty.all(0),
              backgroundColor: MaterialStateProperty.all(Colors.black12),
              shape: MaterialStateProperty.all(RoundedRectangleBorder(
                borderRadius: BorderRadius.circular(35),
              )),
            ),
            child: Text(
              _msg,
              style: (widget.active && _numberCount.avaliable)
                  ? availableStyle
                  : unAvailableStyle,
            ),
            onPressed: () {
              _numberCount.start();
              widget.onPressed!();
            },
          );
        });
  }
}	

通过Streambuilder中的builder的回调函数,来获取最新的NumberCount中的数据。

回到main.dart,先来获取手机号输入部分的内容,并来进行正则判断:

TextField(
  decoration: InputDecoration(
    prefixIcon: Icon(
      MyIcons.person,
      color: Colors.black54,
      size: 26,
    ),
  ),
  onChanged: (val) {
    RegExp exp = RegExp(r'^1[3-9]\d{9}');
    if (exp.hasMatch(val)) {
      setState(() {
        _active = true;
      });
    } else {
      setState(() {
        // 设置button的状态
        _active = false;
      });
    }
    // print('val is 👉 $val');
  },
),

// ...
Stack(
  children: [
    TextField(
      decoration: InputDecoration(
        prefixIcon: Icon(
          Icons.lock,
          color: Colors.black54,
        ),
      ),
    ),
    // Positioned
    // 1.页面按钮状态 -> 有状态组件如何设置页面状态
    // 2.倒计时逻辑 -> stream
    Positioned(
      right: 10.0,
      child: CounterButton(
        active: _active,
        onPressed: () {
          // todo
        },
      ),
    )
  ],
),