Flutter Todos Tutorial with “flutter_bloc”
⚠️ This article may be out of date. Please view the updated tutorial at bloclibrary.dev.
In the following tutorial, we’re going to build a Todos App in Flutter using the Bloc Library. By the time we’re done, our app should look something like this:
Let’s get started!
Setup
We’ll start off by creating a brand new Flutter project
flutter create flutter_todos
We can then replace the contents of pubspec.yaml
with:
and finally install all of our dependencies
flutter packages get
Note: We’re overriding some dependencies because we’re going to be reusing them from Brian Egan’s Flutter Architecture Samples.
Todos Repository
In this tutorial we’re not going to go into the implementation details of the TodosRepository
because it was implemented by Brian Egan and is shared among all of the Todo Architecture Samples.
At a high level, the TodosRepository
will expose a method to loadTodos
and to saveTodos
. That's pretty much all we need to know so for the rest of the tutorial we'll focus on the Bloc and Presentation layers.
Todos Bloc
Our
TodosBloc
will be responsible for convertingTodosEvents
intoTodosStates
and will manage the list of todos in our application.
Model
The first thing we need to do is define our Todo
model. Each todo will need to have an id, a task, an optional note, and an optional completed flag.
Let’s create a models
directory and create todo.dart
.
Note: We’re using the Equatable package so that we can compare instances of Todos
without having to manually override ==
and hashCode
.
Next up, we need to create the TodosState
which our presentation layer will receive.
States
Let’s create blocs/todos/todos_state.dart
and define the different states we'll need to handle.
The three states we will implement are:
TodosLoading
- the state while our application is fetching todos from the repository.TodosLoaded
- the state of our application after the todos have successfully been loaded.TodosNotLoaded
- the state of our application if the todos were not successfully loaded.
Note: We are annotating our base TodosState
with the immutable decorator so that we can indicate that all TodosStates
cannot be changed.
Next, let’s implement the events we will need to handle.
Events
The events we will need to handle in our TodosBloc
are:
LoadTodos
- tells the bloc that it needs to load the todos from theTodosRepository
.AddTodo
- tells the bloc that it needs to add an new todo to the list of todos.UpdateTodo
- tells the bloc that it needs to update an existing todo.DeleteTodo
- tells the bloc that it needs to remove an existing todo.ClearCompleted
- tells the bloc that it needs to remove all completed todos.ToggleAll
- tells the bloc that it needs to toggle the completed state of all todos.
Create blocs/todos/todos_event.dart
and let's implement the events we described above.
Now that we have our TodosStates
and TodosEvents
implemented we can implement our TodosBloc
.
Bloc
Let’s create blocs/todos/todos_bloc.dart
and get started! We just need to implement initialState
and mapEventToState
.
Tip: Check out the Bloc VSCode Extension which provides tools for effectively creating blocs for both Flutter and AngularDart apps.
When we yield a state in the private mapEventToState
handlers, we are always yielding a new state instead of mutating the currentState
. This is because every time we yield, bloc will compare the currentState
to the nextState
and will only trigger a state change (transition
) if the two states are not equal. If we just mutate and yield the same instance of state, then currentState == nextState
would evaluate to true and no state change would occur.
Our TodosBloc
will have a dependency on the TodosRepository
so that it can load and save todos. It will have an initial state of TodosLoading
and defines the private handlers for each of the events. Whenever the TodosBloc
changes the list of todos it calls the saveTodos
method in the TodosRepository
in order to keep everything persisted locally.
Barrel File
Now that we’re done with our TodosBloc
we can create a barrel file to export all of our bloc files and make it convenient to import them later on.
Create blocs/todos/todos.dart
and export the bloc, events, and states:
Filtered Todos Bloc
The
FilteredTodosBloc
will be responsible for reacting to state changes in theTodosBloc
we just created and will maintain the state of filtered todos in our application.
Model
Before we start defining and implementing the TodosStates
, we will need to implement a VisibilityFilter
model that will determine which todos our FilteredTodosState
will contain. In this case, we will have three filters:
all
- show all Todos (default)active
- only show Todos which have not been completedcompleted
only show Todos which have been completed
We can create models/visibility_filter.dart
and define our filter as an enum:
States
Just like we did with the TodosBloc
, we'll need to define the different states for our FilteredTodosBloc
.
In this case, we only have two states:
FilteredTodosLoading
- the state while we are fetching todosFilteredTodosLoaded
- the state when we are no longer fetching todos
Let’s create blocs/filtered_todos/filtered_todos_state.dart
and implement the two states.
Note: The FilteredTodosLoaded
state contains the list of filtered todos as well as the active visibility filter.
Events
We’re going to implement two events for our FilteredTodosBloc
:
UpdateFilter
- which notifies the bloc that the visibility filter has changedUpdateTodos
- which notifies the bloc that the list of todos has changed
Create blocs/filtered_todos/filtered_todos_event.dart
and let's implement the two events.
We’re ready to implement our FilteredTodosBloc
next!
Bloc
Our FilteredTodosBloc
will be similar to our TodosBloc
; however, instead of having a dependency on the TodosRepository
, it will have a dependency on the TodosBloc
itself. This will allow the FilteredTodosBloc
to update its state in response to state changes in the TodosBloc
.
Create blocs/filtered_todos/filtered_todos_bloc.dart
and let's get started.
We create a StreamSubscription
for the stream of TodosStates
so that we can listen to the state changes in the TodosBloc
. We override the bloc's dispose method and cancel the subscription so that we can clean up after the bloc is disposed.
Barrel File
Just like before, we can create a barrel file to make it more convenient to import the various filtered todos classes.
Create blocs/filtered_todos/filtered_todos.dart
and export the three files:
Stats Bloc
The
StatsBloc
will be responsible for maintaining the statistics for number of active todos and number of completed todos. Similarly, to theFilteredTodosBloc
, it will have a dependency on theTodosBloc
itself so that it can react to changes in theTodosBloc
state.
State
Our StatsBloc
will have two states that it can be in:
StatsLoading
- the state when the statistics have not yet been calculated.StatsLoaded
- the state when the statistics have been calculated.
Create blocs/stats/stats_state.dart
and let's implement our StatsState
.
Next, let’s define and implement the StatsEvents
.
Events
There will just be a single event our StatsBloc
will respond to: UpdateStats
. This event will be dispatched whenever the TodosBloc
state changes so that our StatsBloc
can recalculate the new statistics.
Create blocs/stats/states_event.dart
and let's implement it.
Now we’re ready to implement our StatsBloc
which will look very similar to the FilteredTodosBloc
.
Bloc
Our StatsBloc
will have a dependency on the TodosBloc
itself which will allow it to update its state in response to state changes in the TodosBloc
.
Create blocs/stats/stats_bloc.dart
and let's get started.
That’s all there is to it! Our StatsBloc
recalculates its state which contains the number of active todos and the number of completed todos on each state change of our TodosBloc
.
Now that we’re done with the StatsBloc
we just have one last bloc to implement: the TabBloc
.
Tab Bloc
The
TabBloc
will be responsible for maintaining the state of the tabs in our application. It will be takingTabEvents
as input and outputtingAppTabs
.
Model / State
We need to define an AppTab
model which we will also use to represent the TabState
. The AppTab
will just be an enum
which represents the active tab in our application. Since the app we're building will only have two tabs: todos and stats, we just need two values.
Create models/app_tab.dart
:
Event
Our TabBloc
will be responsible for handling a single TabEvent
:
UpdateTab
- which notifies the bloc that the active tab has updated
Create blocs/tab/tab_event.dart
:
Bloc
Our TabBloc
implementation will be super simple. As always, we just need to implement initialState
and mapEventToState
.
Create blocs/tab/tab_bloc.dart
and let's quickly do the implementation.
I told you it’d be simple. All the TabBloc
is doing is setting the initial state to the todos tab and handling the UpdateTab
event by yielding a new AppTab
instance.
Barrel File
Lastly, we’ll create another barrel file for our TabBloc
exports. Create blocs/tab/tab.dart
and export the two files:
Bloc Delegate
Before we move on to the presentation layer, we will implement our own BlocDelegate
which will allow us to handle all state changes and errors in a single place. It's really useful for things like developer logs or analytics.
Create blocs/simple_bloc_delegate.dart
and let's get started.
All we’re doing in this case is printing all state changes (transitions
) and errors to the console just so that we can see what's going on when we're running our app locally. You can hook up your BlocDelegate
to Google Analytics, Sentry, Crashlytics, etc...
Blocs Barrel File
Now that we have all of our blocs implemented we can create a barrel file. Create blocs/blocs.dart
and export all of our blocs so that we can conveniently import any bloc code with a single import.
Up next, we’ll focus on implementing the major screens in our Todos application.
Screens
Home Screen
Our
HomeScreen
will be responsible for creating theScaffold
of our application. It will maintain theAppBar
,BottomNavigationBar
, as well as theStats
/FilteredTodos
widgets (depending on the active tab).
Let’s create a new directory called screens
where we will put all of our new screen widgets and then create screens/home_screen.dart
.
Our HomeScreen
will be a StatefulWidget
because it will need to create and dispose the TabBloc
, FilteredTodosBloc
, and StatsBloc
.
The HomeScreen
creates the TabBloc
, FilteredTodosBloc
, and StatsBloc
as part of its state. It uses BlocProvider.of<TodosBloc>(context)
in order to access the TodosBloc
which will be made available from our root TodosApp
widget (we'll get to it later in this tutorial).
Since the HomeScreen
needs to respond to changes in the TodosBloc
state, we use BlocBuilder
in order to build the correct widget based on the current TodosState
.
The HomeScreen
also makes the TabBloc
, FilteredTodosBloc
, and StatsBloc
available to the widgets in its subtree by using the BlocProviderTree
widget from flutter_bloc.
BlocProviderTree(
blocProviders: [
BlocProvider<TabBloc>(bloc: _tabBloc),
BlocProvider<FilteredTodosBloc>(bloc: _filteredTodosBloc),
BlocProvider<StatsBloc>(bloc: _statsBloc),
],
child: Scaffold(...),
);
is equivalent to writing
BlocProvider<TabBloc>(
bloc: _tabBloc,
child: BlocProvider<FilteredTodosBloc>(
bloc: _filteredTodosBloc,
child: BlocProvider<StatsBloc>(
bloc: _statsBloc,
child: Scaffold(...),
),
),
);
You can see how using BlocProviderTree
helps reduce the levels of nesting and makes the code easier to read and maintain.
Next, we’ll implement the DetailsScreen
.
Details Screen
The
DetailsScreen
displays the full details of the selected todo and allows the user to either edit or delete the todo.
Create screens/details_screen.dart
and let's build it.
Note: The DetailsScreen
requires a todo id so that it can pull the todo details from the TodosBloc
and so that it can update whenever a todo's details have been changed (a todo's id cannot be changed).
The main things to note are that there is an IconButton
which dispatches a DeleteTodo
event as well as a checkbox which dispatches an UpdateTodo
event.
There is also another FloatingActionButton
which navigates the user to the AddEditScreen
with isEditing
set to true
. We'll take a look at the AddEditScreen
next.
Add/Edit Screen
The
AddEditScreen
widget allows the user to either create a new todo or update an existing todo based on theisEditing
flag that is passed via the constructor.
Create screens/add_edit_screen.dart
and let's have a look at the implementation.
There’s nothing bloc-specific in this widget. It’s simply presenting a form and:
- if
isEditing
is true the form is populated it with the existing todo details. - otherwise the inputs are empty so that the user can create a new todo.
It uses an onSave
callback function to notify its parent of the updated or newly created todo.
That’s it for the screens in our application so before we forget, let’s create a barrel file to export them.
Screens Barrel File
Create screens/screens.dart
and export all three.
Widgets
FilterButton
The
FilterButton
widget will be responsible for providing the user with a list of filter options and will notify theFilteredTodosBloc
when a new filter is selected.
Let’s create a new directory called widgets
and put our FilterButton
implementation in widgets/filter_button.dart
.
The FilterButton
needs to respond to state changes in the FilteredTodosBloc
so it uses BlocProvider
to access the FilteredTodosBloc
from the BuildContext
. It then uses BlocBuilder
to re-render whenever the FilteredTodosBloc
changes state.
The rest of the implementation is pure Flutter and there isn’t much going on so we can move on to the ExtraActions
widget.
Extra Actions
Similarly to the
FilterButton
, theExtraActions
widget is responsible for providing the user with a list of extra options: Toggling Todos and Clearing Completed Todos.
Since this widget doesn’t care about the filters it will interact with the TodosBloc
instead of the FilteredTodosBloc
.
Let’s create widgets/extra_actions.dart
and implement it.
Just like with the FilterButton
, we use BlocProvider
to access the TodosBloc
from the BuildContext
and BlocBuilder
to respond to state changes in the TodosBloc
.
Based on the action selected, the widget dispatches an event to the TodosBloc
to either ToggleAll
todos' completion states or ClearCompleted
todos.
Next we’ll take a look at the TabSelector
widget.
Tab Selector
The
TabSelector
widget is responsible for displaying the tabs in theBottomNavigationBar
and handling user input.
Let’s create widgets/tab_selector.dart
and implement it.
You can see that there is no dependency on blocs in this widget; it just calls onTabSelected
when a tab is selected and also takes an activeTab
as input so it knows which tab is currently selected.
Next, we’ll take a look at the FilteredTodos
widget.
Filtered Todos
The
FilteredTodos
widget is responsible for showing a list of todos based on the current active filter.
Create widgets/filtered_todos.dart
and let's implement it.
Just like the previous widgets we’ve written, the FilteredTodos
widget uses BlocProvider
to access blocs (in this case both the FilteredTodosBloc
and the TodosBloc
are needed).
- The
FilteredTodosBloc
is needed to help us render the correct todos based on the current filter - The
TodosBloc
is needed to allow us to add/delete todos in response to user interactions such as swiping on an individual todo.
Todo Item
TodoItem
is a stateless widget which is responsible for rendering a single todo and handling user interactions (taps/swipes).
Create widgets/todo_item.dart
and let's build it.
Again, notice that the TodoItem
has no bloc-specific code in it. It simply renders based on the todo we pass via the constructor and calls the injected callback functions whenever the user interacts with the todo.
Next up, we’ll create the DeleteTodoSnackBar
.
Delete Todo SnackBar
The
DeleteTodoSnackBar
is responsible for indicating to the user that a todo was deleted and allows the user to undo his/her action.
Create widgets/delete_todo_snack_bar.dart
and let's implement it.
By now, you’re probably noticing a pattern: this widget also has no bloc-specific code. It simply takes in a todo in order to render the task and calls a callback function called onUndo
if a user presses the undo button.
We’re almost done; just two more widgets to go!
Loading Indicator
The
LoadingIndicator
widget is a stateless widget that is responsible for indicating to the user that something is in progress.
Create widgets/loading_indicator.dart
and let's write it.
Not much to discuss here; we’re just using a CircularProgressIndicator
wrapped in a Center
widget (again no bloc-specific code).
Lastly, we need to build our Stats
widget.
Stats
The
Stats
widget is responsible for showing the user how many todos are active (in progress vs completed.
Let’s create widgets/stats.dart
and take a look at the implementation.
We’re accessing the StatsBloc
using BlocProvider
and using BlocBuilder
to rebuild in response to state changes in the StatsBloc
state.
Putting it all together
Let’s create main.dart
and our TodosApp
widget. We need to create a main
function and run our TodosApp
.
Note: We are setting our BlocSupervisor’s delegate to the SimpleBlocDelegate
we created earlier so that we can hook into all transitions and errors.
Next, let’s implement our TodosApp
widget.
Our TodosApp
is a stateless widget which creates a TodosBloc
and makes it available through the entire application by using the BlocProvider
widget from flutter_bloc.
The TodosApp
has two routes:
Home
- which renders aHomeScreen
AddTodo
- which renders aAddEditScreen
withisEditing
set tofalse
.
The entire main.dart
should look like this:
That’s all there is to it! We’ve now implemented a todos app in flutter using the bloc and flutter_bloc packages and we’ve successfully separated our presentation layer from our business logic.
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.