# 获取验证码
# 需求分析
- 用户输入手机号,如果输入不正确,无法点击获取验证码;
- 用于点击获取验证码 -> 进入倒计时
- 倒计时结束才能再次发送获取验证码
# 初步封装按钮组件
需求&步骤:
创建文件:
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 sum
与int duration
,定义方法start
,stop
,dispose
(销毁计数器,回收内存); - 定义一个
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
},
),
)
],
),
← 状态管理方案-Bloc 封装请求 →