Or is it really that simple?
So I was working on a project where I needed to implement a simple stopwatch to let the user know about the time interval spent doing a particular task. Okay, how difficult will that be right? Dart already has a Stopwatch() class that I can use directly.
But as I started working on it, I realized how awfully wrong I was and how such a simple feature is not that easy to implement and how the Stopwatch() class in Dart really sucks! (That is only one person’s opinion. It may not suck for you). And since I am a lazy person, I thought okay lets just search “create a stopwatch in flutter” and then copy the code and paste it. But OH MY GOD! how complex was the code. It was like 1000 lines of code for a simple stopwatch feature. (And its bad practice to use code that you can’t understand).
So instead of going through all those 1000 lines of code I decided to write the stopwatch feature myself in as simple way as possible. After 2 hours of coding and a broken glass I was able to manage something like that on the left side.
A Simple Stopwatch with a Start and Reset Button.
And it was accomplished in a fairly low amount of code.( I hope you have the same opinion too after reading the article. Fingers Crossed)
My Thought Process on building The Feature:
Let me take you first through my thought process. I am going to write exactly what was going through my mind (If you want to directly go to the code then you may skip this part).
Okay So I need a timer which looks like 00:00. It should start with a START button and go back to zero with a RESET Button. Ummm… Dart has a stopwatch library right (which I used earlier to calculate time taken by a particular bunch of code). I can use that directly right and display it on the screen. BOOM. I just have to use a Text widget, start the stopwatch (using the Stopwatch() class) and display the elapsed time on Screen.
But wait. I will need to read a different value in the Text Widget every second. So I need to get the data as a Stream. But Unfortunately Stopwatch() class of dart has no method to get the values as a stream. It only outputs a single value whenever one of its methods is called.
So I can’t use it. Okay this is where I realized that this is not going to be as easy as I initially thought.
New Plan. I need to create a Stream which outputs a new value every second. Kind of Like 1,2,3,4,5,6,… Then I can listen to the stream and update the values in the Text Widget. Problem solved (at-least in my mind).
But wait. There is another problem. What happens when the value crossed 60. I can’t show 61,62,63,etc. right. I need to format the values and change it minutes and seconds so that I can display them beautifully.
Lets Get to the Code Now
Step 1: Creating a Stopwatch Stream
First we will create a stream which will give us the elapsed time in seconds after every second so that we can update it to the UI.
We defined a method called stopWatchStream() which will return a Stream of integers.
We will be using a StreamController to control the events of the Stream.
StreamControllergives you a new stream and a way to add events to the stream at any point, and from anywhere. The stream has all the logic necessary to handle listeners and pausing. You return the stream and keep the controller to yourself.
When instantiating a StreamController we need to pass few parameters there. First is onListen which will be called whenever we want to listen to the Stream. Next is OnCancel which will be called on cancelling the Stream. We will get to OnResume & OnPause in a bit.
In OnListen we passed a method startTimer. Lets see what startTimer is doing. startTimer is creating a new instance of Timer.periodic(). Timer.periodic() takes two parameters: an interval and a callback function. And what it does is after every interval it calls the given callback function again & again until the timer is cancelled. Here, Timer.periodic() will call the tick() function after every 1 second.
You may use a different interval to instead of 1 second. Just change the Duration in timerInterval variable
Now lets see what the tick() function is doing. The tick function simply adds +1 to the counter and then adds the counter to the stream. Whenever we are listening to the stream, we will get this “counter” every time the streamcontroller.add() method is called.
Okay now lets look at the stopTimer() method which will be called on cancelling the stream. We are doing a list of things here. First we check if the timer is not null, i.e. there must be a running instance of timer. If someone calls the stopTimer() method without starting the timer first then timer will be equal to null.
Next we cancel the timer. Once cancelled it will stop running the tick function every second.
We also set it to null so that it starts from scratch and doesn’t start from the place it ended last time.
Next we set the counter to 0 so that it starts from 0 too next time the timer is started.
And finally we close the Stream using streamcontroller.close()
Step 2: Adding the timer stream to the UI
We have 3 primary widgets here:-
- Text Widget to display the Time
- A Start Button
- A Reset Button
The Text widget is simple. It is simply displaying the time in HH:MM:SS format.
Now lets look at the Start Button.
A lot is going on in here.
timerStream = stopWatchStream();
First we are creating a new stream from our stopWatchStream() that we created previously and set it to var timerStream. Now you might be thinking why am I creating a new stream every time START button is pressed. I could create a new Stream in initState directly too right and that would be more efficient.
A BIG NO!
If you do that then yes when you click on the Start Button first time everything will work fine. But if you click on the Start button a 2nd time then you will get an error like this
This is because dart somehow allows you to listen to a stream only once even after cancelling the subscription to the stream.
One workaround around this problem is to use a BroadCast stream. But problem is it doesn’t close the stream fully. It only pauses the stream in a way and then resumes from the same place we left earlier when started again.
So in our stopwatch when I click on the START button, I want the timer to go like 1,2,3,4,5… and then clicking on the reset button should reset the timer to 0. If I then click on the START button a second time, it must start again from 0,1,2,3,4,5 and so on. But if you are using a broadcast stream what happens is if you click on the RESET button the timer will go back to 0 in the UI but in the background it will still be running. So when you click on the START button a second time, instead of starting from 0 again it will start from 11,12,13 or something like that(the number of seconds that has passed since you clicked on the START button first time).
So to fix this bug I just instantiate a new Stream every time START button is clicked.
Listening to timerStream
Next I listen on the Stream. So here after every second I will be getting a new tick value (like 1,2,3,4,5,6,7). But we directly cannot show the tick values in the UI. We have to transform it to HH:MM:SS format.
As you can see inside the listen function we are using 3 String variables hoursSTR, minutesStr and secondsStr. In each one of it we are using an algorithm to transform the tick to hours, minutes, seconds respectively and then updating the variables inside a setState() function.
Let me explain the formatting in the secondsStr with an example:
secondsStr = (newTick % 60).floor().toString().padLeft(2, ‘0’);
Suppose the newTick value is 81. Ofcourse we can’t show 81 because there are only 60 seconds in a clock. And after seconds must start from 1 again till 60 and so on. So 81 seconds should be shown as 21 in our stopwatch according to the HH:MM:SS format. Lets see how our code achieves that.
First 81 % 60 = 21
.floor() simply changes it to int. (I know its already int, just as a extra measure. Prevention is better than cure right!)
Next convert it to String using toString()
Well padLeft(2, ‘0’) will have no effect on 21. But if it was a single digit number then it will pad a 0 on the left of the number. (For example -> 4 to 04)
Well minutesStr is also same just the difference being that we are first dividing the tick value to 60 to convert from seconds to minutes.
Similarly in hoursStr we are dividing it by 3600 to convert to hours.
(You may also do Days by dividing by 3600*24 or years 3600*24*365)
Finally the RESET Button
We just have to cancel the subscription (timerSubscription.cancel()) to the stream here which calls stopTimer() method internally which I already explained above.
I also set the timerStream to null just as a extra measure.
And thats it. We have our stopwatch.
Clicking on the START button starts the timer and the RESET button resets the time back to ZERO.
You can find the complete code in github here: https://github.com/realdiganta/Flutter-Stopwatch
Please let me know If I made any mistakes in the code or while explaining the code or if you think that I can make the code simpler in any way. Because that was my primary motive behind this: To build a StopWatch in Flutter with as few lines of code as possible.
If I was able to add value to your day in any way please don’t forget to clap for the article. That really encourages me to write more articles like this.
You may contact me here: email@example.com