记录flutter实现一个弹窗倒计时踩的坑

1,434 阅读2分钟

前言

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();
  }
}

1.git.gif

可以看到 data 其实是实时变化,但是弹窗的值是并没有实时跟着变化。

问题

为什么会这样? 需要注意的是 已经使用setState了, data的值确实也实时变化了,页面上的值已经看到跟着变化,而弹窗的值没有跟着变化。

思考

值肯定是变化了,截图中也看到在页面上显示的数字实时变化了。原因影响就是CupertinoAlertDialog 这个弹窗,进入源码 发现

image.png 很明显看到这里其实使用路由,反过来说明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();
  }
}


效果

2.gif

注意事项

  1. dispose 勾子中 需要清除定时器

  2. 每次点击弹窗 需要重置 StreamController, 否则会提示 bad Stream 已经被监听

  3. stram流中数据在页面异步更新ui,可以使用 StreamBuilder方式,同理如果不考虑页面本身元素渲染,而只是实现弹窗的 倒计时

    setState(() { controller.add(data = timerNumer.toString());// 流中添加元素 });

    可以改为:

    controller.add(data = timerNumer.toString());// 流中添加元素

    同理源码中其他几个 setState 也可以去掉

结语

项目源码 github.com/chenbing11/… master分支 有需要请自取

创作不易,请多点赞+关注,谢谢! 有问题留言,我都会及时回复