Flutter Todos Tutorial with “flutter_bloc”
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!
We’ll start off by creating a brand new Flutter project
flutter create flutter_todos
We can then replace the contents of
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.
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.
TodosBlocwill be responsible for converting
TodosStatesand will manage the list of todos in our application.
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
Note: We’re using the Equatable package so that we can compare instances of
Todos without having to manually override
Next up, we need to create the
TodosState which our presentation layer will receive.
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.
The events we will need to handle in our
LoadTodos- tells the bloc that it needs to load the todos from the
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.
blocs/todos/todos_event.dart and let's implement the events we described above.
Now that we have our
TodosEvents implemented we can implement our
blocs/todos/todos_bloc.dart and get started! We just need to implement
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.
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.
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.
blocs/todos/todos.dart and export the bloc, events, and states:
Filtered Todos Bloc
FilteredTodosBlocwill be responsible for reacting to state changes in the
TodosBlocwe just created and will maintain the state of filtered todos in our application.
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 completed
completedonly show Todos which have been completed
We can create
models/visibility_filter.dart and define our filter as an enum:
Just like we did with the
TodosBloc, we'll need to define the different states for our
In this case, we only have two states:
FilteredTodosLoading- the state while we are fetching todos
FilteredTodosLoaded- the state when we are no longer fetching todos
blocs/filtered_todos/filtered_todos_state.dart and implement the two states.
FilteredTodosLoaded state contains the list of filtered todos as well as the active visibility filter.
We’re going to implement two events for our
UpdateFilter- which notifies the bloc that the visibility filter has changed
UpdateTodos- which notifies the bloc that the list of todos has changed
blocs/filtered_todos/filtered_todos_event.dart and let's implement the two events.
We’re ready to implement 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
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.
Just like before, we can create a barrel file to make it more convenient to import the various filtered todos classes.
blocs/filtered_todos/filtered_todos.dart and export the three files:
StatsBlocwill be responsible for maintaining the statistics for number of active todos and number of completed todos. Similarly, to the
FilteredTodosBloc, it will have a dependency on the
TodosBlocitself so that it can react to changes in the
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.
blocs/stats/stats_state.dart and let's implement our
Next, let’s define and implement the
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.
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
StatsBloc will have a dependency on the
TodosBloc itself which will allow it to update its state in response to state changes in the
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
Now that we’re done with the
StatsBloc we just have one last bloc to implement: the
TabBlocwill be responsible for maintaining the state of the tabs in our application. It will be taking
TabEventsas input and outputting
Model / State
We need to define an
AppTab model which we will also use to represent 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.
TabBloc will be responsible for handling a single
UpdateTab- which notifies the bloc that the active tab has updated
TabBloc implementation will be super simple. As always, we just need to implement
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
Lastly, we’ll create another barrel file for our
TabBloc exports. Create
blocs/tab/tab.dart and export the two files:
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.
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.
HomeScreenwill be responsible for creating the
Scaffoldof our application. It will maintain the
BottomNavigationBar, as well as the
FilteredTodoswidgets (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
HomeScreen will be a
StatefulWidget because it will need to create and dispose the
HomeScreen creates the
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).
HomeScreen needs to respond to changes in the
TodosBloc state, we use
BlocBuilder in order to build the correct widget based on the current
HomeScreen also makes the
StatsBloc available to the widgets in its subtree by using the
BlocProviderTree widget from flutter_bloc.
is equivalent to writing
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
DetailsScreendisplays the full details of the selected todo and allows the user to either edit or delete the todo.
screens/details_screen.dart and let's build it.
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
There is also another
FloatingActionButton which navigates the user to the
isEditing set to
true. We'll take a look at the
AddEditScreenwidget allows the user to either create a new todo or update an existing todo based on the
isEditingflag that is passed via the constructor.
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:
isEditingis 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
screens/screens.dart and export all three.
FilterButtonwidget will be responsible for providing the user with a list of filter options and will notify the
FilteredTodosBlocwhen a new filter is selected.
Let’s create a new directory called
widgets and put our
FilterButton implementation in
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
Similarly to the
ExtraActionswidget 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
widgets/extra_actions.dart and implement it.
Just like with the
FilterButton, we use
BlocProvider to access the
TodosBloc from the
BlocBuilder to respond to state changes in the
Based on the action selected, the widget dispatches an event to the
TodosBloc to either
ToggleAll todos' completion states or
Next we’ll take a look at the
TabSelectorwidget is responsible for displaying the tabs in the
BottomNavigationBarand handling user input.
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
FilteredTodoswidget is responsible for showing a list of todos based on the current active filter.
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).
FilteredTodosBlocis needed to help us render the correct todos based on the current filter
TodosBlocis needed to allow us to add/delete todos in response to user interactions such as swiping on an individual todo.
TodoItemis a stateless widget which is responsible for rendering a single todo and handling user interactions (taps/swipes).
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
Delete Todo SnackBar
DeleteTodoSnackBaris responsible for indicating to the user that a todo was deleted and allows the user to undo his/her action.
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!
LoadingIndicatorwidget is a stateless widget that is responsible for indicating to the user that something is in progress.
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
Statswidget is responsible for showing the user how many todos are active (in progress vs completed.
widgets/stats.dart and take a look at the implementation.
We’re accessing the
BlocProvider and using
BlocBuilder to rebuild in response to state changes in the
Putting it all together
main.dart and our
TodosApp widget. We need to create a
main function and run our
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 is a stateless widget which creates a
TodosBloc and makes it available through the entire application by using the
BlocProvider widget from flutter_bloc.
TodosApp has two routes:
Home- which renders a
AddTodo- which renders a
main.dart should look like this:
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.