Flutter Timer with “flutter_bloc”
⚠️ This article may be out of date. Please view the updated tutorial at bloclibrary.dev.
Hey everyone! In today’s tutorial we’re going to cover how to build a timer application using flutter_bloc. The finished application should look like this:
Let’s get started!
Setup
We’ll start off by creating a brand new Flutter project
flutter create flutter_timer
We can then replace the contents of pubspec.yaml
with:
Note: We’ll be using the flutter_bloc, equatable, and wave packages in this app.
Next, run flutter packages get
to install all the dependencies.
Ticker
Start off by creating ticker.dart
. The ticker will be our data source for the timer application. It will expose a stream of ticks
which we can subscribe and react to.
All our Ticker
class does is expose a tick
function which takes the number of ticks (seconds) we want and returns a stream which emits the remaining seconds every second.
Next up, we need to create our TimerBloc
which will consume the Ticker
.
Timer Bloc
TimerState
We’ll start off by defining the TimerStates
which our TimerBloc
can be in.
Our TimerBloc
state can be one of the following:
- Ready —ready to start counting down from the specified duration.
- Running — actively counting down from the specified duration
- Paused — paused at some remaining duration
- Finished — completed with a remaining duration of 0
Each of these states will have an implication on what the user sees.
For example:
- if the state is “ready,” the user will be able to start the timer.
- if the state is “running,” the user will be able to pause and reset the timer as well as see the remaining duration.
- if the state is “paused,” the user will be able to resume the timer and reset the timer.
- if the state is “finished,” the user will be able to reset the timer.
In order to keep all of our bloc files together, let’s create a bloc
directory with bloc/timer_state.dart
.
Tip: You can use the IntelliJ or VSCode extensions to autogenerate the following bloc files for you.
Note that all of the TimerStates
extend the abstract base class TimerState
which has a duration
property. This is because no matter what state our TimerBloc
is in, we want to know how much time is remaining.
Next up, let’s define and implement the TimerEvents
which our TimerBloc
will be processing.
TimerEvent
Our TimerBloc
will need to know how to process the following events:
- Start — informs the
TimerBloc
that the timer should be started. - Pause — informs the
TimerBloc
that the timer should be paused. - Resume — informs the
TimerBloc
that the timer should be resumed. - Reset — informs the
TimerBloc
that the timer should be reset to the original state. - Tick — informs the
TimerBloc
that a tick has occurred and that it needs to update its state accordingly.
If you didn’t use the IntelliJ or VSCode extensions, then create bloc/timer_event.dart
and let’s implement those events.
Next up, let’s implement the TimerBloc
!
TimerBloc
If you haven’t already, create bloc/timer_bloc.dart
and create a empty TimerBloc
.
The first thing we need to do is define the initialState
of our TimerBloc
. In this case, we want the TimerBloc
to start off in the Ready
state with a preset duration of 1 minute (60 seconds).
Next, we need to define the dependency on our Ticker
.
We are also defining a StreamSubscription
for our Ticker
which we will get to in a bit.
At this point, all that’s left to do is implement mapEventToState
. For improved readability, I like to break out each event handler into its own helper function. We’ll start with the Start
event.
If the TimerBloc
receives a Start
event, it pushes a Running
state with the start duration. In addition, if there was already an open _tickerSubscription
we need to cancel it to deallocate the memory. We also need to override the dispose
method on our TimerBloc
so that we can cancel the _tickerSubscription
when the TimerBloc
is disposed. Lastly, we listen to the _ticker.tick
stream and on every tick we dispatch a Tick
event with the remaining duration.
Next, let’s implement the Tick
event handler.
Every time a Tick
event is received, if the tick’s duration is greater than 0, we need to push an updated Running
state with the new duration. Otherwise, if the tick’s duration is 0, our timer has ended and we need to push a Finished
state.
Now let’s implement the Pause
event handler.
In _mapPauseToState
if the currentState
of our TimerBloc
is Running
, then we can pause the _tickerSubscription
and push a Paused
state with the current timer duration.
Next, let’s implement the Resume
event handler so that we can unpause the timer.
The Resume
event handler is very similar to the Pause
event handler. If the TimerBloc
has a currentState
of Paused
and it receives a Resume
event, then it resumes the _tickerSubscription
and pushes a Running
state with the current duration.
Lastly, we need to implement the Reset
event handler.
If the TimerBloc
receives a Reset
event, it needs to cancel the current _tickerSubscription
so that it isn’t notified of any additional ticks and pushes a Ready
state with the original duration.
If you didn’t use the IntelliJ or VSCode extensions be sure to create bloc/bloc.dart
in order to export all the bloc files and make it possible to use a single import for convenience.
That’s all there is to the TimerBloc
. Now all that’s left is implement the UI for our Timer Application.
Application UI
MyApp
We can start off by deleting the contents of main.dart
and creating our MyApp
widget which will be the root of our application.
MyApp
is a StatefulWidget
because it needs to manage initializing and disposing an instance of TimerBloc
. In addition, it’s using the BlocProvider
widget in order to make our TimerBloc
instance available to the widgets in our subtree.
Next, we need to implement our Timer
widget.
Timer
Our Timer
widget will be responsible for displaying the remaining time along with the proper buttons which will enable users to start, pause, and reset the timer.
So far, we’re just using BlocProvider
to access the instance of our TimerBloc
and using a BlocBuilder
widget in order to rebuild the UI every time we get a new TimerState
.
Next, we’re going to implement our Actions
widget which will have the proper actions (start, pause, and reset).
Actions
The Actions
widget is just another StatelessWidget
which uses BlocProvider
to access the TimerBloc
instance and then returns different FloatingActionButtons
based on the current state of the TimerBloc
. Each of the FloatingActionButtons
dispatches an event in its onPressed
callback to notify the TimerBloc
.
Now we need to hook up the Actions
to our Timer
widget.
We added another BlocBuilder
which will render the Actions
widget; however, this time we’re using a newly introduced flutter_bloc feature to control how frequently the Actions
widget is rebuilt (introduced in v0.15.0).
If you want fine-grained control over when the builder function is called you can provide an optional condition
to BlocBuilder
. The condition
takes the previous bloc state and current bloc state and returns a boolean
. If condition
returns true, builder
will be called with currentState
and the widget will rebuild. If condition
returns false, builder
will not be called with currentState
and no rebuild will occur.
In this case, we don’t want the Actions
widget to be rebuilt on every tick because that would be inefficient. Instead, we only want Actions
to rebuild if the runtimeType
of the TimerState
changes (Ready
=> Running
, Running
=> Paused
, etc...).
As a result, if we randomly colored the widgets on every rebuild, it would look like:
Even though the Text
widget is rebuilt on every tick, we only rebuild the Actions
if they need to be rebuilt.
Lastly, we need to add the super cool wave background using the wave package.
Waves Background
Putting it all together
Our finished, main.dart
should look like:
That’s all there is to it! At this point we have a pretty solid timer application which efficiently rebuilds only widgets that need to be rebuilt.
The full source for this example can be found here.
If you enjoyed this exercise as much as I did you can support me by ⭐️the repository, or 👏 for this story.