前言
flutter 中利用CupertinoAlertDialog 弹窗,实现弹窗倒计时,记录一下倒计时数值不会实时变化的问题
环境
macOS 10.15.7
Flutter 2.0.6
Xcode Version 12.4
vscode
依赖
pubspec.yaml
dependencies:
flutter:
sdk: flutter
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.2
flutter_custom_dialog: ^1.0.20
实现一个简单的 CupertinoAlertDialog 弹窗
import 'package:flutter/material.dart';
import 'dart:async';
import 'package:flutter/cupertino.dart';
class CountDownOldPage extends StatefulWidget {
@override
_CountDownOldPageState createState() => _CountDownOldPageState();
}
class _CountDownOldPageState extends State<CountDownOldPage> {
String data = ''; // 实时显示的值
dynamic _timer = null;//定义一个计时器
int timerNumer = 3; //倒计时总数(单位为秒)
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("倒计时"),
),
body: Center(
child: GestureDetector(
onTap: (){
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.max,
children: [
RaisedButton(
padding: EdgeInsets.all(0),
onPressed: () {
data = '';
buildRedisterSuccDialog(context); //打开定时器弹窗
},
child: Row(
children: [
Text('倒计时弹窗'),
],
),
),
Text(
data,
style: TextStyle(fontSize: 28),
textAlign: TextAlign.center,
),
Container(
height: 18.0,
),
]
),
),
),
);
}
//登录成功倒计时弹窗
void buildRedisterSuccDialog(BuildContext context) {
setState(() {
data = timerNumer.toString();
});
startCountDown(
timerNumer,
(value) {
setState(() {
data = value;
});
},
() { //定时器结束
setState(() {
data = '0';
});
Navigator.of(context).pop(true); //关闭对话框
}
);//倒计时
// 这里有坑,必须是 StreamBuilder 数据流的形式,直接设置setState 数据不行
showCupertinoDialog(
context: context,
builder: (BuildContext context) {
return CupertinoAlertDialog(
title: Text(
'倒计时',
style: TextStyle(fontWeight: FontWeight.bold, ),
),
content: Container(
child: Column(
children:[
SizedBox(height:12.0),
Text(
'倒计时文本1',
style: TextStyle(fontSize: 10.0,
color: Color(0xff333333)),
textAlign: TextAlign.center
),
SizedBox(height:4.0),
]
)
),
actions: <Widget>[
Container(
child: FlatButton(
child: Text("倒计时($data)" , style: TextStyle(
fontSize: 20.0,
color: Color(0xff315efb),
),),
onPressed: () async {
Navigator.of(context).pop(true); //关闭对话框
clearTimer();//清除定时器
//to do
}
),
),
],
);
},
);
}
/**
* 倒计时方法,
* setValueFun 每间隔一秒执行回调重置值;
* callBackFun定时器结束是的回调函数
*/
void startCountDown(int time, Function setValueFun, Function callBackFun) {
// 重新计时的时候要把之前的清除掉
clearTimer();
if (time <= 0) {
return;
}
var countTime = time;
const repeatPeriod = const Duration(seconds: 1);
_timer = Timer.periodic(repeatPeriod, (timer) {
if (countTime <= 0) {
clearTimer();
//倒计时结束,可以在这里做相应的操
callBackFun(); //定时器结束时的回调函数
return;
}
countTime--;
//外面传进来的单位是秒,所以需要根据总秒数,计算小时,分钟,秒
int hour = (countTime ~/ 3600) % 24;
int minute = countTime % 3600 ~/60;
int second = countTime % 60;
String str = '';
if (hour > 0) {
str = str + hour.toString()+':';
}
if(minute > 0) {
if (minute / 10 < 1) {//当只有个位数时,给前面加“0”,实现效果:“:01”,":02"
str = str + '0' + minute.toString() + ":";
} else {
str = str + minute.toString() + ":";
}
}
if (second / 10 < 1) {
if(hour == 0 && minute == 0) {
str = str + second.toString();
} else {
str = str + '0' + second.toString();
}
} else {
str = str + second.toString();
}
setValueFun(str);
});
}
//清除倒计时
void clearTimer(){
if (_timer != null) {
// if (_timer.isActive) {
_timer.cancel();
_timer = null;
// }
}
}
@override
void dispose() {
super.dispose();
clearTimer();
}
}
可以看到 data 其实是实时变化,但是弹窗的值是并没有实时跟着变化。
问题
为什么会这样? 需要注意的是 已经使用setState了, data的值确实也实时变化了,页面上的值已经看到跟着变化,而弹窗的值没有跟着变化。
思考
值肯定是变化了,截图中也看到在页面上显示的数字实时变化了。原因影响就是CupertinoAlertDialog 这个弹窗,进入源码 发现
很明显看到这里其实使用路由,反过来说明CupertinoAlertDialog 弹窗其实是跳转了一个新的路由,还有从关闭弹窗的方法
Navigator.of(context).pop(true); //关闭对话框
也可以看出来CupertinoAlertDialog 是一个新的路由页面。从这里就明白了了数字为什么没有实时变化。传入的值在在传入的那瞬间传进去了,setState重新渲染的是当前路由页面的,其他的页面并不会实时响应,如果跟我一样是web开发者来说,容易踩这个坑。同样以web开发理解,如果这里是一个响应式的数据,就是data的值是存在数据状态管理中,那是不是就可以解决该问题。
CupertinoAlertDialog 弹窗 -- 改进版
这里就一个文件,所以我就不引入类似rxDart 响应式插件(当然是是一样可以实现),我就是flutter内置的 stream流来解决。
源码如下 :
import 'package:flutter/material.dart';
import 'dart:async';
import 'package:flutter/cupertino.dart';
class CountDownNewPage extends StatefulWidget {
@override
_CountDownNewPageState createState() => _CountDownNewPageState();
}
class _CountDownNewPageState extends State<CountDownNewPage> {
StreamController<String> controller =StreamController(); // 定义一个StreamController
String data = ''; //实时显示的值
dynamic _timer = null;//定义一个计时器
int timerNumer = 3; //倒计时总数(单位为秒)
@override
void initState() {
super.initState();
setStream();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("倒计时"),
),
body: Center(
child: GestureDetector(
onTap: (){
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.max,
children: [
RaisedButton(
padding: EdgeInsets.all(0),
onPressed: () {
setStream(); //再次点击需要重置流,否则提示bad state
buildRedisterSuccDialog(context); //打开定时器弹窗
},
child: Row(
children: [
Text('倒计时弹窗'),
],
),
),
Text(
data,
style: TextStyle(fontSize: 28),
textAlign: TextAlign.center,
),
Container(
height: 18.0,
),
]
),
),
),
);
}
//登录成功倒计时弹窗
void buildRedisterSuccDialog(BuildContext context) {
setState(() {
controller.add(data = timerNumer.toString());// 流中添加元素
});
// controller.add(data = timerNumer.toString());// 流中添加元素
startCountDown(
timerNumer,
(value) {
setState(() {
controller.add(data = value);
});
},
() { //定时器结束
setState(() {
controller.add(data = '0');
});
Navigator.of(context).pop(true); //关闭对话框
}
);//倒计时
showCupertinoDialog(
context: context,
builder: (BuildContext context) {
return CupertinoAlertDialog(
title: Text(
'倒计时',
style: TextStyle(fontWeight: FontWeight.bold, ),
),
content: Container(
child: Column(
children:[
SizedBox(height:12.0),
Text(
'倒计时文本1',
style: TextStyle(fontSize: 10.0,
color: Color(0xff333333)),
textAlign: TextAlign.center
),
SizedBox(height:4.0),
]
)
),
actions: <Widget>[
Container(
child: StreamBuilder<String>(
stream: controller.stream,
initialData: '',
builder: (BuildContext context, AsyncSnapshot snapshot) {
return FlatButton(
child: Text("倒计时(${snapshot.data})" , style: TextStyle(
fontSize: 20.0,
color: Color(0xff315efb),
),),
onPressed: () async {
Navigator.of(context).pop(true); //关闭对话框
clearTimer();//清除定时器
//to do
}
);
}
)
),
],
);
},
);
}
/**
* 倒计时方法,
* setValueFun 每间隔一秒执行回调重置值;
* callBackFun定时器结束是的回调函数
*/
void startCountDown(int time, Function setValueFun, Function callBackFun) {
// 重新计时的时候要把之前的清除掉
clearTimer();
if (time <= 0) {
return;
}
var countTime = time;
const repeatPeriod = const Duration(seconds: 1);
_timer = Timer.periodic(repeatPeriod, (timer) {
if (countTime <= 0) {
clearTimer();
//倒计时结束,可以在这里做相应的操
callBackFun(); //定时器结束时的回调函数
return;
}
countTime--;
//外面传进来的单位是秒,所以需要根据总秒数,计算小时,分钟,秒
int hour = (countTime ~/ 3600) % 24;
int minute = countTime % 3600 ~/60;
int second = countTime % 60;
String str = '';
if (hour > 0) {
str = str + hour.toString()+':';
}
if(minute > 0) {
if (minute / 10 < 1) {//当只有个位数时,给前面加“0”,实现效果:“:01”,":02"
str = str + '0' + minute.toString() + ":";
} else {
str = str + minute.toString() + ":";
}
}
if (second / 10 < 1) {
if(hour == 0 && minute == 0) {
str = str + second.toString();
} else {
str = str + '0' + second.toString();
}
} else {
str = str + second.toString();
}
setValueFun(str);
});
}
//清除倒计时
void clearTimer(){
if (_timer != null) {
// if (_timer.isActive) {
_timer.cancel();
_timer = null;
// }
}
}
// 重设置 数据流
void setStream () {
data = '';
//第一步:构造数据数据的控制器,用于往流中添加数据
controller = StreamController();
}
@override
void dispose() {
super.dispose();
clearTimer();
}
}
效果
注意事项
-
dispose 勾子中 需要清除定时器
-
每次点击弹窗 需要重置 StreamController, 否则会提示 bad Stream 已经被监听
-
stram流中数据在页面异步更新ui,可以使用 StreamBuilder方式,同理如果不考虑页面本身元素渲染,而只是实现弹窗的 倒计时
setState(() { controller.add(data = timerNumer.toString());// 流中添加元素 });可以改为:
controller.add(data = timerNumer.toString());// 流中添加元素同理源码中其他几个 setState 也可以去掉
结语
项目源码 github.com/chenbing11/… master分支 有需要请自取
创作不易,请多点赞+关注,谢谢! 有问题留言,我都会及时回复