Flutter - Restart CountdownTimer - flutter

I have a countdown timer (5 mins). I need something to restart this countdown and start from the beginning.
I try to change state of countdown variable but doesn´t works, stops and restarting from the last number of counter.
My code:
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:quiver/async.dart';
class ProgressIndicatorDemo extends StatefulWidget {
#override
_ProgressIndicatorDemoState createState() =>
new _ProgressIndicatorDemoState();
}
class _ProgressIndicatorDemoState extends State<ProgressIndicatorDemo>
with SingleTickerProviderStateMixin {
AnimationController controller;
Animation<double> animation;
var countdown;
int actual;
#override
void initState() {
super.initState();
animationStart();
startTimer();
}
#override
void dispose() {
controller.dispose();
super.dispose();
}
void animationStart() {
controller =
AnimationController(duration: Duration(minutes: 5), vsync: this);
animation = Tween(begin: 0.0, end: 1.0).animate(controller)
..addListener(() {
setState(() {});
});
controller.forward();
}
void startTimer() {
CountdownTimer countDownTimer = new CountdownTimer(
new Duration(minutes: 5),
new Duration(seconds: 1),
);
countdown = countDownTimer.listen(null);
countdown.onData((duration) {
setState(() {
actual = 300 - duration.elapsed.inSeconds;
});
});
countdown.onDone(() {
countdown.cancel();
});
}
#override
Widget build(BuildContext context) {
return new Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
animation.value != 1.0
? CircularProgressIndicator(
semanticsLabel:
(animation.value * 100.0).toStringAsFixed(1).toString() +
'%',
semanticsValue:
(animation.value * 100.0).toStringAsFixed(1).toString() +
'%',
backgroundColor: Colors.black.withOpacity(0.25),
valueColor:
new AlwaysStoppedAnimation<Color>(Colors.green[700]),
value: animation.value)
: Icon(Icons.info_outline),
actual != null
? Text(
"Tiempo:" +
DateFormat.ms()
.format(DateTime.fromMillisecondsSinceEpoch(
actual * 1000))
.toString(),
textAlign: TextAlign.center,
)
: Container(),
FlatButton(
child: Text('Reset ' + actual.toString()), onPressed: () {}),
],
),
);
}
}
So the question is: How can i restart from the beggining the countdown?

You could use a cleaner solution by using the RestartableTimer
import 'package:async/async.dart';
RestartableTimer _timer = new RestartableTimer(_timerDuration, _startNewPage);
Then you can restart your countdown by simply calling _timer.reset();
Hope it helps.

Try cancel the timer first and start the timer again :
void restartTimer() {
countDownTimer.cancel();
startTimer();
}

try to declare your Timer at the top and whenever you want to restart it call this method.
void restartTimer() {
countDownTimer = new CountdownTimer(
new Duration(minutes: 5),
new Duration(seconds: 1),
);
don't forget to declare the variable

Related

building a countdown clock inside a provider package change notifier

I have built a working countdown clock that displays the time remaining in the format, hours:minutes:senconds. inside a stateful widget. that uses a fullscreen inkwell to start and stop it.
What I want to do is transfer this code to a change notifier. (I already a change notifier setup ready to go called CountdownTimers) so i can see this countdown running from multiple pages on my app.
Here is the code for the working countdown clock in the stateful widget:
class ChessClock2 extends StatefulWidget {
const ChessClock2({Key? key}) : super(key: key);
#override
State<ChessClock2> createState() => _ChessClock2State();
}
class _ChessClock2State extends State<ChessClock2> {
static const countdownDuration = Duration(hours: 5, minutes: 10, seconds: 10);
Duration duration = Duration();
Timer? timer;
bool beenPressed = false;
#override
void initState() {
super.initState();
Reset();
}
void Reset() {
setState(() => duration = countdownDuration);
}
void AddTime() {
final minusSeconds = -1;
setState(() {
final seconds = duration.inSeconds + minusSeconds;
if (seconds < 0) {
timer?.cancel();
} else {
duration = Duration(seconds: seconds);
}
});
}
void StartTimer() {
timer = Timer.periodic(
Duration(seconds: 1),
(_) => AddTime(),
);
}
void StopTimer() {
setState(() => timer?.cancel());
}
#override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
body: InkWell(
onTap: () {
setState(() {
beenPressed = !beenPressed;
});
beenPressed ? StartTimer() : StopTimer();
},
child: Container(
width: double.infinity,
height: double.infinity,
decoration: BoxDecoration(
color: beenPressed ? kOrange : kBlueGrey900,
borderRadius: BorderRadius.circular(30),
),
child: Center(
child: TimeDisplay(),
),
),
),
),
);
}
Widget TimeDisplay() {
String twoDigits(int n) => n.toString().padLeft(2, '0');
final hours = twoDigits(
duration.inHours.remainder(60),
);
final minutes = twoDigits(
duration.inMinutes.remainder(60),
);
final seconds = twoDigits(
duration.inSeconds.remainder(60),
);
return Text(
'$hours:$minutes:$seconds',
style: TextStyle(fontSize: 50),
);
}
}
When I transfer the code over, I'm running into trouble because I can't use setState in the change notifier and I'm unsure how to translate the code to get it to work.
so far, by moving the individual variables over as well as the widget TimDisplay, I'm able to get the timer to display correctly from the change notifier but am not sure how to get it to work from the change notifier.
here is where I am now:
type hereclass ChessClock3 extends StatefulWidget {
const ChessClock3({Key? key}) : super(key: key);
#override
State<ChessClock3> createState() => _ChessClock3State();
}
class _ChessClock3State extends State<ChessClock3> {
#override
void initState() {
super.initState();
Reset();
}
void Reset() {
setState(() => duration = countdownDuration);
}
void AddTime() {
final minusSeconds = -1;
setState(() {
final seconds = duration.inSeconds + minusSeconds;
if (seconds < 0) {
timer?.cancel();
} else {
duration = Duration(seconds: seconds);
}
});
}
void StartTimer() {
timer = Timer.periodic(
Duration(seconds: 1),
(_) => AddTime(),
);
}
void StopTimer() {
setState(() => timer?.cancel());
}
#override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
body: InkWell(
onTap: () {
setState(() {
context.read<CountdownTimers>().BeenPressed();
});
context.read<CountdownTimers>().beenPressed
? StartTimer()
: StopTimer();
},
child: Container(
width: double.infinity,
height: double.infinity,
decoration: BoxDecoration(
color: context.watch<CountdownTimers>().beenPressed
? kKillTeamOrange
: kBlueGrey900,
borderRadius: BorderRadius.circular(30),
),
child: Center(
child: context.read<CountdownTimers>().TimeDisplay(),
),
),
),
),
);
}
}
class CountdownTimers with ChangeNotifier {
Duration _countdownDuration = Duration(hours: 5, minutes: 10, seconds: 10);
Duration _duration = Duration();
Timer? timer;
bool _beenPressed = false;
Duration get countdownDuration => _countdownDuration;
Duration get duration => _duration;
bool get beenPressed => _beenPressed;
void BeenPressed() {
_beenPressed = !_beenPressed;
}
Widget TimeDisplay() {
String twoDigits(int n) => n.toString().padLeft(2, '0');
final hours = twoDigits(
_duration.inHours.remainder(60),
);
final minutes = twoDigits(
_duration.inMinutes.remainder(60),
);
final seconds = twoDigits(
_duration.inSeconds.remainder(60),
);
return Text(
'$hours:$minutes:$seconds',
style: TextStyle(fontSize: 50),
);
}
}
If anyone can show me how to translate the code over It would be very much appreciated.
thanks so much!
I would use Flutter Riverpod instead like the code below. In Riverpod it's not recommended to use Change Notifier. Even in complexe application. To change the state of Change Notifier you must call notifyListeners.
import 'dart:async';
import 'package:flutter_riverpod/flutter_riverpod.dart';
final countDownControllerProvider = StateNotifierProvider.family
.autoDispose<CountdownController, Duration, Duration>((ref, initialDuration) {
return CountdownController(initialDuration);
});
class CountdownController extends StateNotifier<Duration> {
Timer? timer;
final Duration initialDuration;
CountdownController(this.initialDuration) : super(initialDuration) {
startTimer();
}
void startTimer() {
timer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (state == Duration.zero) {
timer.cancel();
} else {
if (mounted) {
state = state - const Duration(seconds: 1);
} else {
timer.cancel();
}
}
});
}
void stopTimer() {
timer?.cancel();
}
void resetTimer({required Duration initialDuration}) {
stopTimer();
state = initialDuration;
startTimer();
}
void addTime({required Duration duration}) {
state = state + duration;
}
void subtractTime({required Duration duration}) {
state = state - duration;
}
#override
void dispose() {
timer?.cancel();
super.dispose();
}
}
Then in the widget
Consumer(builder: (context, ref, _) {
return Text(
ref
.watch(countDownControllerProvider(
const Duration(
days: 25,
hours: 15,
minutes: 36,
seconds: 45)))
.formatDuration(),
style: context.theme.textTheme.bodyText1!
.copyWith(color: Colors.white),
);
})
And finally, don't hesitate to put your conversion logic Duration to String, into a extension
extension DurationExtensions on Duration {
String formatDuration() {
int seconds = inSeconds;
final days = seconds~/Duration.secondsPerDay;
seconds -= days*Duration.secondsPerDay;
final hours = seconds~/Duration.secondsPerHour;
seconds -= hours*Duration.secondsPerHour;
final minutes = seconds~/Duration.secondsPerMinute;
seconds -= minutes*Duration.secondsPerMinute;
final List<String> tokens = [];
if (days != 0) {
tokens.add('${days}d');
}
if (tokens.isNotEmpty || hours != 0){
tokens.add('${hours}h');
}
if (tokens.isNotEmpty || minutes != 0) {
tokens.add('${minutes}min');
}
if(tokens.isNotEmpty || seconds != 0) {
tokens.add('${seconds}s');
}
return tokens.join('');
}
}
The trick you are looking for is the function notifyListeners() of a ChangeNotifier. If you used context.watch() inside your widget to watch the ChangeNotifier, you will be updated and the widget is rebuild if necessary.
A short example might look like this
ConsumingWidget.dart
#override
Widget build(BuildContext context) {
var timeProvider = context.watch<TimeProvider>();
int counter = timeProvider.counter;
return Text("${counter} seconds have passed");
}
In this case, you would need to provide an instance of TimeProvider above your main widget in the widget tree via ChangeNotifierProvider.
You can then let the widget rebuild (if .counter changed) via notifyListeners()
ParentWidget.dart
#override
Widget build(BuildContext context) {
ChangeNotifierProvider(
create: (_) => TimeProvider(),
child: ConsumingWidget()
);
}
Provider.dart
class TimeProvider extends ChangeNotifier {
Timer? timer;
final Duration initialDuration;
int counter = 0;
CountdownController(this.initialDuration) : super(initialDuration) {
startTimer();
}
void startTimer() {
timer = Timer.periodic(const Duration(seconds: 1), (timer) {
counter += 1; // Change some data based on the timer. Just an example here
notifyListeners(); // Then update listeners!
});
}
#override
void dispose() {
timer?.cancel();
super.dispose();
}
This is the built-in solution of the Provider package. Starting from this basic pattern, you can then look into more sophisticated state management solutions like Riverpod, Bloc etc.

Postpone the start of the AnimationController animation in Flutter

I'm trying to delay the animation for 5 seconds from the beginning, here's my code snippet, but it doesn't work properly, as it would be better to delay for 5 seconds from the start of the startup screen. I tried to make a separate function that would be responsible for the delay but it also caused me to display errors on the emulator. I tried to use a ternary operator and delay it, but it also didn't work properly. I also want my animation to come in 7 seconds, I think it can also be done in a ternary operator. I don't yet know exactly how to stop the animation, so I used a long delay in the animation to make it invisible.
class BubbleBackground extends StatefulWidget {
final Widget child;
final double size;
const BubbleBackground({
Key key,
#required this.child,
#required this.size,
}) : super(key: key);
#override
State<BubbleBackground> createState() => _BubbleBackgroundState();
}
class _BubbleBackgroundState extends State<BubbleBackground>
with SingleTickerProviderStateMixin {
bool timer = false;
final longAnimationDuration = 100000;
final shortAnimationDuration = 700;
AnimationController _controller;
#override
void initState() {
setState(() {
// Future.delayed(Duration(seconds: 5), () {
// timer = true;
// });
timer = true;
});
_controller = AnimationController(
lowerBound: 0.9,
duration: Duration(
milliseconds: timer
? Future.delayed(
Duration(seconds: 5),
() {
shortAnimationDuration;
},
)
: longAnimationDuration,
),
vsync: this,
)..repeat(reverse: true);
super.initState();
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (_, __) {
return Transform.scale(
scale: _controller.value,
child: Container(
width: widget.size,
height: widget.size,
alignment: Alignment.center,
decoration: BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
),
child: widget.child,
),
);
},
);
}
}
i figured out that I could just write initState normally, and I'll get desirable result for time delay
#override
void initState() {
super.initState();
_controller = AnimationController(
lowerBound: 0.9,
duration: Duration(milliseconds: shortAnimationDuration),
vsync: this,
);
Future.delayed(Duration(seconds: 5), () {
_controller.repeat(reverse: true);
});
}

Flutter: Perform an action when a countdown timer reaches 0

I've created a screen in Flutter that displays a countdown timer. I'm able to play, pause and restart the timer, but I am trying to figure out how to perform an action when the timer reaches 0 (for example, restart itself).
As the dart file is fairly lengthy, I'm just copying below what I believe to be the relevant portions here. But I can add more if needed.
First I create a widget/class for the countdown timer:
class Countdown extends AnimatedWidget {
Countdown({ Key key, this.animation }) : super(key: key, listenable: animation);
Animation<int> animation;
#override
build(BuildContext context){
return Text(
animation.value.toString(),
style: TextStyle(
fontSize: 120,
color: Colors.deepOrange
),
);
}
}
I then have a stateful widget which creates the controller and also imports some data (gameData.countdownClock is the countdown timer's start time, it comes from user input at an earlier screen):
class _GameScreenState extends State<GameScreen> with TickerProviderStateMixin {
AnimationController _controller;
_GameScreenState(this.gameData);
GameData gameData;
#override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: Duration(seconds: gameData.countdownClock),
);
}
And then the container that displays the clock:
Container(
child: Countdown(
animation: StepTween(
begin: gameData.countdownClock,
end: 0,
).animate(_controller),
),
),
Do I have to add a listener in that last container? Or somewhere else? (Or something else entirely!)
Any help is appreciated. Thank you
I found the answer on this page:
After complete the widget animation run a function in Flutter
I needed to add .addStatusListener to the animation controller in the initState().
So the new initState() code looks like this:
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: Duration(seconds: gameData.countdownClock),
);
_controller.addStatusListener((status){
if(status == AnimationStatus.completed){
_controller.reset();
}
}
);
}
Possible value of Animation controller is between 0 to 1.
So I think you have to add listener on gamedata.countDownClock
Please check the below code you might get some idea from it.
import 'dart:async';
import 'package:flutter/material.dart';
class GameScreen extends StatefulWidget {
#override
_GameScreenState createState() => _GameScreenState();
}
class _GameScreenState extends State<GameScreen> with SingleTickerProviderStateMixin {
int countdownClock = 10;
#override
void initState() {
super.initState();
// Your Game Data Counter Change every one Second
const oneSec = const Duration(seconds:1);
new Timer.periodic(oneSec, (Timer t) {
// Restart The Counter Logic
if (countdownClock == 0)
countdownClock = 11;
setState(() { countdownClock = countdownClock - 1; });
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("")),
body: Center(
child: Text('$countdownClock')
),
);
}
}

Flutter: Ticker must be disposed before calling super.dispose()

I don't know why this error is appearing in the console box
Console message:
SplashScreenState created a Ticker via its SingleTickerProviderStateMixin, but at the time dispose() was called on the mixin, that Ticker was still active. The Ticker must be disposed before calling super.dispose().
Tickers used by AnimationControllers should
be disposed by calling dispose() on
the AnimationController itself. Otherwise,
the ticker will leak.
The offending ticker was:
Ticker(created by SplashScreenState#dae31(lifecycle state: created))
HERE IS MY FULL CODE FOR SPLASHSCREEN:
import 'dart:async';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
class SplashScreen extends StatefulWidget {
#override
SplashScreenState createState() => new SplashScreenState();
}
class SplashScreenState extends State<SplashScreen>
with SingleTickerProviderStateMixin {
var _visible = true;
AnimationController animationController;
Animation<double> animation;
startTime() async {
var _duration = new Duration(seconds: 3);
return new Timer(_duration, navigationPage);
}
void navigationPage() {
Navigator.of(context).pushReplacementNamed(HOME_SCREEN);
}
#override
void initState() {
super.initState();
animationController = new AnimationController(
vsync: this,
duration: new Duration(seconds: 2),
);
animation =
new CurvedAnimation(parent: animationController, curve: Curves.easeOut);
animation.addListener(() => this.setState(() {}));
animationController.forward();
setState(() {
_visible = !_visible;
});
startTime();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
fit: StackFit.expand,
children: <Widget>[
new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new Image.asset(
'assets/vegan1.png',
width: animation.value * 280,
height: animation.value * 280,
),
],
),
],
),
);
}
}
How can I solve this error. Please answer if you have any solution or idea to solve this.only added important points are added reduce the size of code. If you need more console code then please comment.
Override dispose method and dispose the AnimationController instance.
#override
dispose() {
animationController.dispose(); // you need this
super.dispose();
}

Triggering widget animation from outside widget

I have a custom widget that has normal / animated state. Sometimes I want to be it animated, and sometimes static.
I have made a simple test project to demonstrate my problem: test page contains my custom widget (ScoreBoard) and 2 buttons to start / stop animating scoreboard. My problem, that ScoreBoard is not animated, even if I start animation.
Here is my code:
TestPage:
class TestPage extends StatefulWidget {
#override
_TestPageState createState() => _TestPageState();
}
class _TestPageState extends State<TestPage> {
bool _animate;
#override
void initState() {
_animate = false;
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
ScoreBoard(
text: "Hello, world!",
animate: _animate,
),
FlatButton(
child: Text("START animation"),
onPressed: () {
setState(() {
_animate = true;
});
},
),
FlatButton(
child: Text("STOP animation"),
onPressed: () {
setState(() {
_animate = false;
});
},
),
],
),
);
}
}
ScoreBoard widget:
class ScoreBoard extends StatefulWidget {
final String text;
final bool animate;
const ScoreBoard({Key key, this.text, this.animate}) : super(key: key);
#override
_ScoreBoardState createState() => _ScoreBoardState();
}
class _ScoreBoardState extends State<ScoreBoard>
with SingleTickerProviderStateMixin {
AnimationController _controller;
#override
void initState() {
super.initState();
_controller = new AnimationController(
duration: const Duration(seconds: 1),
vsync: this,
)..forward();
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return widget.animate
? ScaleTransition(
child:
Text(widget.text, style: Theme.of(context).textTheme.display1),
scale: new CurvedAnimation(
parent: _controller,
curve: Curves.easeIn,
),
)
: Container(
child:
Text(widget.text, style: Theme.of(context).textTheme.display1),
);
}
}
Would you be so kind to help me? Thanks in advance!
Answer
If you initialize an AnimationController widget and do not specify arguments for lowerBound and upperBound (which is the case here), your animation is going to start by default with lowerBound 0.0.
AnimationController({double value, Duration duration, String debugLabel, double lowerBound: 0.0, double upperBound: 1.0, AnimationBehavior animationBehavior: AnimationBehavior.normal, #required TickerProvider vsync })
Creates an animation controller. [...]
https://docs.flutter.io/flutter/animation/AnimationController-class.html
If you initialize the state of your widget ScoreBoard, the method forward gets called only once during the whole lifetime of your app. The method forward makes that your animation increases from the lowerBound (0.0) to the upperBound (1.0) within 1 second.
Starts running this animation forwards (towards the end).
https://docs.flutter.io/flutter/animation/AnimationController/forward.html
In our case, there is no way back once the method forward got called. We are only able to stop the animation.
Press Ctrl + F5 for a full restart of the app to see the animation.
To make it even clearer, change the duration of your animation to 10 seconds.
Btw. since Dart 2 you do not need to use the new keyword.
#override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(seconds: 10),
vsync: this,
)..forward();
}
Example
To see what happens you could add this to your build method:
#override
Widget build(BuildContext context) {
// Execute 'reverse' if the animation is completed
if (_controller.isCompleted)
_controller.reverse();
else if (_controller.isDismissed)
_controller.forward();
// ...
... and do not call the method forward in the initState method:
#override
void initState() {
super.initState();
_controller = new AnimationController(
duration: const Duration(seconds: 10),
vsync: this,
);
}