12

I am trying to build a countdown widget. Currently, I got the structure to work. I only struggle with the countdown itself. I tried this approach using the countdown plugin:

class _Countdown extends State<Countdown> {

  int val = 3;

  void countdown(){
    CountDown cd = new CountDown(new Duration(seconds: 4));

    cd.stream.listen((Duration d) {
      setState((){
        val = d.inSeconds;
      });
    });

  }

  @override
  build(BuildContext context){
    countdown();
    return new Scaffold(
      body: new Container(
        child: new Center(
          child: new Text(val.toString(), style: new TextStyle(fontSize: 150.0)),
        ),
      ),
    );
  }
}

However, the value changes very weirdly and not smooth at all. It start twitching. Any other approach or fixes?

OhMad
  • 6,871
  • 20
  • 56
  • 85
  • Can you elaborate on where you are at the moment? How does your code look? – Rainer Wittmann Jun 01 '17 at 10:48
  • Actually, I got the structure to work. The only thing that is buggy is the function itself. I tried it out with Timer and the countdown.dart plugin. I'll share my code – OhMad Jun 01 '17 at 11:20
  • what do you mean by weirdly ? do you need animation ? – Hadrien Lejard Jun 01 '17 at 12:17
  • when do you call `countdown` method? Note, each time you call, it creates new timer, – Valentyn Shybanov Jun 01 '17 at 12:27
  • Oh well, I guess this is the problem... it gets called over and over... how can I call it only once at the beginning? – OhMad Jun 01 '17 at 13:49
  • Where is the countdown plugin located? I googled "flutter countdown plugin" and couldn't find where to download it. thanks – Paul Apr 18 '18 at 02:50
  • Just replace val = d.inSeconds with a properly formatted string such as String timerString(Duration duration) { return '${duration.inMinutes}:${(duration.inSeconds % 60).toString().padLeft( 2, '0')}'; } – Shyju M May 18 '18 at 09:31

5 Answers5

20

It sounds like you are trying to show an animated text widget that changes over time. I would use an AnimatedWidget with a StepTween to ensure that the countdown only shows integer values.

countdown

import 'package:flutter/material.dart';

void main() {
  runApp(new MaterialApp(
    home: new MyApp(),
  ));
}

class Countdown extends AnimatedWidget {
  Countdown({ Key key, this.animation }) : super(key: key, listenable: animation);
  Animation<int> animation;

  @override
  build(BuildContext context){
    return new Text(
      animation.value.toString(),
      style: new TextStyle(fontSize: 150.0),
    );
  }
}

class MyApp extends StatefulWidget {
  State createState() => new _MyAppState();
}

class _MyAppState extends State<MyApp> with TickerProviderStateMixin {
  AnimationController _controller;

  static const int kStartValue = 4;

  @override
  void initState() {
    super.initState();
    _controller = new AnimationController(
      vsync: this,
      duration: new Duration(seconds: kStartValue),
    );
  }

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      floatingActionButton: new FloatingActionButton(
        child: new Icon(Icons.play_arrow),
        onPressed: () => _controller.forward(from: 0.0),
      ),
      body: new Container(
        child: new Center(
          child: new Countdown(
            animation: new StepTween(
              begin: kStartValue,
              end: 0,
            ).animate(_controller),
          ),
        ),
      ),
    );
  }
}
Collin Jackson
  • 110,240
  • 31
  • 221
  • 152
1

The countdown() method should be called from the initState() method of the State object.

class _CountdownState extends State<CountdownWidget> {

  int val = 3;
  CountDown cd;

  @override
  void initState() {
    super.initState();
    countdown();
  }
...

Description of initState() from the Flutter docs:

The framework calls initState. Subclasses of State should override initState to perform one-time initialization that depends on the BuildContext or the widget, which are available as the context and widget properties, respectively, when the initState method is called.

Here is a full working example:

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:countdown/countdown.dart';

void main() {
  runApp(new MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Countdown Demo',
      theme: new ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: new MyHomePage(),
    );
  }
}


class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new CountdownWidget();
  }
}

class _CountdownState extends State<CountdownWidget> {

  int val = 3;
  CountDown cd;

  @override
  void initState() {
    super.initState();
    countdown();
  }

  void countdown(){
    print("countdown() called");
    cd = new CountDown(new Duration(seconds: 4));
    StreamSubscription sub = cd.stream.listen(null);
    sub.onDone(() {
      print("Done");
    });
    sub.onData((Duration d) {
      if (val == d.inSeconds) return;
      print("onData: d.inSeconds=${d.inSeconds}");
      setState((){
        val = d.inSeconds;
      });
    });
  }

  @override
  build(BuildContext context){
    return new Scaffold(
      body: new Container(
        child: new Center(
          child: new Text(val.toString(), style: new TextStyle(fontSize: 150.0)),
        ),
      ),
    );
  }
}

class CountdownWidget extends StatefulWidget {

  @override
  _CountdownState createState() => new _CountdownState();
}
raju-bitter
  • 8,906
  • 4
  • 42
  • 53
  • This code will throw errors if the State is disposed while the countdown is running. In Flutter, it's best to always use StreamBuilder when listening to a stream instead of calling setState() – Collin Jackson Jun 01 '17 at 15:29
  • Good point, just wanted to show what the original problem was. – raju-bitter Jun 01 '17 at 16:29
0

based on @raju-bitter answer, alternative to use async/await on countdown stream

  void countdown() async {
    cd = new CountDown(new Duration(seconds:4));
    await for (var v in cd.stream) {
      setState(() => val = v.inSeconds);
    }
  }
Supawat Pusavanno
  • 2,968
  • 28
  • 21
0

Why not use a simple TweenAnimationBuilder its easy to use and you don't need to manage any stream controllers or worry about using streams and disposing them off etc;

TweenAnimationBuilder<double>(
                duration: Duration(seconds: 10),
                tween: Tween(begin: 100.0, end: 0.0),
                onEnd: () {
                  print('Countdown ended');
                },
                builder: (BuildContext context, double value, Widget child) {
                  return Padding(
                      padding: const EdgeInsets.symmetric(vertical: 5),
                      child: Text('${value.toInt()}',
                          textAlign: TextAlign.center,
                          style: TextStyle(
                              color: Colors.black,
                              fontWeight: FontWeight.bold,
                              fontSize: 40)));
                }),

here's the dartpad example to playaround output:

enter image description here

originally answered here

Mahesh Jamdade
  • 17,235
  • 8
  • 110
  • 131
-1

Countdown example using stream, not using setState(...) therefore its all stateless.

this borrow idea from example flutter_stream_friends

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:countdown/countdown.dart';

void main() {
  runApp(new MyApp());
}

class MyApp extends StatelessWidget {
  static String appTitle = "Count down";

  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: appTitle,
      theme: new ThemeData(
        primarySwatch: Colors.purple,
      ),
      home: new StreamBuilder(
          stream: new CounterScreenStream(5),
          builder: (context, snapshot) => buildHome(
              context,
              snapshot.hasData
                  // If our stream has delivered data, build our Widget properly
                  ? snapshot.data
                  // If not, we pass through a dummy model to kick things off
                  : new Duration(seconds: 5),
              appTitle)),
    );
  }

  // The latest value of the CounterScreenModel from the CounterScreenStream is
  // passed into the this version of the build function!
  Widget buildHome(BuildContext context, Duration duration, String title) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text(title),
      ),
      body: new Center(
        child: new Text(
          'Count down ${ duration.inSeconds }',
        ),
      ),
    );
  }
}

class CounterScreenStream extends Stream<Duration> {
  final Stream<Duration> _stream;

  CounterScreenStream(int initialValue)
      : this._stream = createStream(initialValue);

  @override
  StreamSubscription<Duration> listen(
          void onData(Duration event),
          {Function onError,
          void onDone(),
          bool cancelOnError}) =>
      _stream.listen(onData,
          onError: onError, onDone: onDone, cancelOnError: cancelOnError);

  // The method we use to create the stream that will continually deliver data
  // to the `buildHome` method.
  static Stream<Duration> createStream(int initialValue) {
    var cd = new CountDown(new Duration(seconds: initialValue));
    return cd.stream;
  }
}

The difference from stateful is that reload the app will restart counting. When using stateful, in some cases, it may not restart when reload.

Supawat Pusavanno
  • 2,968
  • 28
  • 21