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 pubspec.yaml
with:
name: flutter_todos
description: A new Flutter project.
environment:
sdk: ">=2.0.0 <3.0.0"
dependencies:
meta: ">=1.1.0 <2.0.0"
equatable: ^0.2.0
flutter_bloc: ^0.7.0
flutter:
sdk: flutter
dependency_overrides:
todos_app_core:
git:
url: git://github.com/brianegan/flutter_architecture_samples
path: todos_app_core
todos_repository_core:
git:
url: git://github.com/brianegan/flutter_architecture_samples
path: todos_repository_core
todos_repository_simple:
git:
url: git://github.com/brianegan/flutter_architecture_samples
path: todos_repository_simple
flutter:
uses-material-design: true
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.
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.
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
.
import 'package:todos_app_core/todos_app_core.dart';
import 'package:meta/meta.dart';
import 'package:equatable/equatable.dart';
import 'package:todos_repository_core/todos_repository_core.dart';
@immutable
class Todo extends Equatable {
final bool complete;
final String id;
final String note;
final String task;
Todo(this.task, {this.complete = false, String note = '', String id})
: this.note = note ?? '',
this.id = id ?? Uuid().generateV4(),
super([complete, id, note, task]);
Todo copyWith({bool complete, String id, String note, String task}) {
return Todo(
task ?? this.task,
complete: complete ?? this.complete,
id: id ?? this.id,
note: note ?? this.note,
);
}
@override
String toString() {
return 'Todo { complete: $complete, task: $task, note: $note, id: $id }';
}
TodoEntity toEntity() {
return TodoEntity(task, id, note, complete);
}
static Todo fromEntity(TodoEntity entity) {
return Todo(
entity.task,
complete: entity.complete ?? false,
note: entity.note,
id: entity.id ?? Uuid().generateV4(),
);
}
}
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.
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.import 'package:equatable/equatable.dart';
import 'package:meta/meta.dart';
import 'package:flutter_todos/models/models.dart';
@immutable
abstract class TodosState extends Equatable {
TodosState([List props = const []]) : super(props);
}
class TodosLoading extends TodosState {
@override
String toString() => 'TodosLoading';
}
class TodosLoaded extends TodosState {
final List<Todo> todos;
TodosLoaded([this.todos = const []]) : super([todos]);
@override
String toString() => 'TodosLoaded { todos: $todos }';
}
class TodosNotLoaded extends TodosState {
@override
String toString() => 'TodosNotLoaded';
}
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 TodosBloc
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.Create blocs/todos/todos_event.dart
and let’s implement the events we described above.
import 'package:equatable/equatable.dart';
import 'package:meta/meta.dart';
import 'package:flutter_todos/models/models.dart';
@immutable
abstract class TodosEvent extends Equatable {
TodosEvent([List props = const []]) : super(props);
}
class LoadTodos extends TodosEvent {
@override
String toString() => 'LoadTodos';
}
class AddTodo extends TodosEvent {
final Todo todo;
AddTodo(this.todo) : super([todo]);
@override
String toString() => 'AddTodo { todo: $todo }';
}
class UpdateTodo extends TodosEvent {
final Todo updatedTodo;
UpdateTodo(this.updatedTodo) : super([updatedTodo]);
@override
String toString() => 'UpdateTodo { updatedTodo: $updatedTodo }';
}
class DeleteTodo extends TodosEvent {
final Todo todo;
DeleteTodo(this.todo) : super([todo]);
@override
String toString() => 'DeleteTodo { todo: $todo }';
}
class ClearCompleted extends TodosEvent {
@override
String toString() => 'ClearCompleted';
}
class ToggleAll extends TodosEvent {
@override
String toString() => 'ToggleAll';
}
Now that we have our TodosStates
and TodosEvents
implemented we can implement our TodosBloc
.
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.
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:meta/meta.dart';
import 'package:flutter_todos/blocs/todos/todos.dart';
import 'package:flutter_todos/models/models.dart';
import 'package:todos_repository_simple/todos_repository_simple.dart';
class TodosBloc extends Bloc<TodosEvent, TodosState> {
final TodosRepositoryFlutter todosRepository;
TodosBloc({@required this.todosRepository});
@override
TodosState get initialState => TodosLoading();
@override
Stream<TodosState> mapEventToState(
TodosState currentState,
TodosEvent event,
) async* {
if (event is LoadTodos) {
yield* _mapLoadTodosToState();
} else if (event is AddTodo) {
yield* _mapAddTodoToState(currentState, event);
} else if (event is UpdateTodo) {
yield* _mapUpdateTodoToState(currentState, event);
} else if (event is DeleteTodo) {
yield* _mapDeleteTodoToState(currentState, event);
} else if (event is ToggleAll) {
yield* _mapToggleAllToState(currentState);
} else if (event is ClearCompleted) {
yield* _mapClearCompletedToState(currentState);
}
}
Stream<TodosState> _mapLoadTodosToState() async* {
try {
final todos = await this.todosRepository.loadTodos();
yield TodosLoaded(
todos.map(Todo.fromEntity).toList(),
);
} catch (_) {
yield TodosNotLoaded();
}
}
Stream<TodosState> _mapAddTodoToState(
TodosState currentState,
AddTodo event,
) async* {
if (currentState is TodosLoaded) {
final List<Todo> updatedTodos = List.from(currentState.todos)
..add(event.todo);
yield TodosLoaded(updatedTodos);
_saveTodos(updatedTodos);
}
}
Stream<TodosState> _mapUpdateTodoToState(
TodosState currentState,
UpdateTodo event,
) async* {
if (currentState is TodosLoaded) {
final List<Todo> updatedTodos = currentState.todos.map((todo) {
return todo.id == event.updatedTodo.id ? event.updatedTodo : todo;
}).toList();
yield TodosLoaded(updatedTodos);
_saveTodos(updatedTodos);
}
}
Stream<TodosState> _mapDeleteTodoToState(
TodosState currentState,
DeleteTodo event,
) async* {
if (currentState is TodosLoaded) {
final updatedTodos =
currentState.todos.where((todo) => todo.id != event.todo.id).toList();
yield TodosLoaded(updatedTodos);
_saveTodos(updatedTodos);
}
}
Stream<TodosState> _mapToggleAllToState(TodosState currentState) async* {
if (currentState is TodosLoaded) {
final allComplete = currentState.todos.every((todo) => todo.complete);
final List<Todo> updatedTodos = currentState.todos
.map((todo) => todo.copyWith(complete: !allComplete))
.toList();
yield TodosLoaded(updatedTodos);
_saveTodos(updatedTodos);
}
}
Stream<TodosState> _mapClearCompletedToState(TodosState currentState) async* {
if (currentState is TodosLoaded) {
final List<Todo> updatedTodos =
currentState.todos.where((todo) => !todo.complete).toList();
yield TodosLoaded(updatedTodos);
_saveTodos(updatedTodos);
}
}
Future _saveTodos(List<Todo> todos) {
return todosRepository.saveTodos(
todos.map((todo) => todo.toEntity()).toList(),
);
}
}
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.
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:
export './todos_bloc.dart';
export './todos_event.dart';
export './todos_state.dart';
Our
TodosBloc
will be responsible for convertingTodosEvents
intoTodosStates
and will manage the list of 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:
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.We can create models/visibility_filter.dart
and define our filter as an enum:
enum VisibilityFilter { all, active, completed }
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:
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.Let’s create blocs/filtered_todos/filtered_todos_state.dart
and implement the two states.
import 'package:equatable/equatable.dart';
import 'package:meta/meta.dart';
import 'package:flutter_todos/models/models.dart';
@immutable
abstract class FilteredTodosState extends Equatable {
FilteredTodosState([List props = const []]) : super(props);
}
class FilteredTodosLoading extends FilteredTodosState {
@override
String toString() => 'FilteredTodosLoading';
}
class FilteredTodosLoaded extends FilteredTodosState {
final List<Todo> filteredTodos;
final VisibilityFilter activeFilter;
FilteredTodosLoaded(this.filteredTodos, this.activeFilter)
: super([filteredTodos, activeFilter]);
@override
String toString() {
return 'FilteredTodosLoaded { filteredTodos: $filteredTodos, activeFilter: $activeFilter }';
}
}
Note*: The* _FilteredTodosLoaded_
state contains the list of filtered todos as well as the active visibility filter.
We’re going to implement two events for our FilteredTodosBloc
:
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.Create blocs/filtered_todos/filtered_todos_event.dart
and let’s implement the two events.
import 'package:equatable/equatable.dart';
import 'package:meta/meta.dart';
import 'package:flutter_todos/models/models.dart';
@immutable
abstract class FilteredTodosEvent extends Equatable {
FilteredTodosEvent([List props = const []]) : super(props);
}
class UpdateFilter extends FilteredTodosEvent {
final VisibilityFilter filter;
UpdateFilter(this.filter) : super([filter]);
@override
String toString() => 'UpdateFilter { filter: $filter }';
}
class UpdateTodos extends FilteredTodosEvent {
final List<Todo> todos;
UpdateTodos(this.todos) : super([todos]);
@override
String toString() => 'UpdateTodos { todos: $todos }';
}
We’re ready to implement our FilteredTodosBloc
next!
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.
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:meta/meta.dart';
import 'package:flutter_todos/blocs/filtered_todos/filtered_todos.dart';
import 'package:flutter_todos/blocs/todos/todos.dart';
import 'package:flutter_todos/models/models.dart';
class FilteredTodosBloc extends Bloc<FilteredTodosEvent, FilteredTodosState> {
final TodosBloc todosBloc;
StreamSubscription todosSubscription;
FilteredTodosBloc({@required this.todosBloc}) {
todosSubscription = todosBloc.state.listen((state) {
if (state is TodosLoaded) {
dispatch(UpdateTodos((todosBloc.currentState as TodosLoaded).todos));
}
});
}
@override
FilteredTodosState get initialState {
return todosBloc.currentState is TodosLoaded
? FilteredTodosLoaded(
(todosBloc.currentState as TodosLoaded).todos,
VisibilityFilter.all,
)
: FilteredTodosLoading();
}
@override
Stream<FilteredTodosState> mapEventToState(
FilteredTodosState currentState,
FilteredTodosEvent event,
) async* {
if (event is UpdateFilter) {
yield* _mapUpdateFilterToState(currentState, event);
} else if (event is UpdateTodos) {
yield* _mapTodosUpdatedToState(currentState, event);
}
}
Stream<FilteredTodosState> _mapUpdateFilterToState(
FilteredTodosState currentState,
UpdateFilter event,
) async* {
if (todosBloc.currentState is TodosLoaded) {
yield FilteredTodosLoaded(
_mapTodosToFilteredTodos(
(todosBloc.currentState as TodosLoaded).todos,
event.filter,
),
event.filter,
);
}
}
Stream<FilteredTodosState> _mapTodosUpdatedToState(
FilteredTodosState currentState,
UpdateTodos event,
) async* {
final visibilityFilter = currentState is FilteredTodosLoaded
? currentState.activeFilter
: VisibilityFilter.all;
yield FilteredTodosLoaded(
_mapTodosToFilteredTodos(
(todosBloc.currentState as TodosLoaded).todos,
visibilityFilter,
),
visibilityFilter,
);
}
List<Todo> _mapTodosToFilteredTodos(
List<Todo> todos, VisibilityFilter filter) {
return todos.where((todo) {
if (filter == VisibilityFilter.all) {
return true;
} else if (filter == VisibilityFilter.active) {
return !todo.complete;
} else if (filter == VisibilityFilter.completed) {
return todo.complete;
}
}).toList();
}
@override
void dispose() {
todosSubscription.cancel();
super.dispose();
}
}
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.
Create blocs/filtered_todos/filtered_todos.dart
and export the three files:
export './filtered_todos_bloc.dart';
export './filtered_todos_event.dart';
export './filtered_todos_state.dart';
Our
TodosBloc
will be responsible for convertingTodosEvents
intoTodosStates
and will manage the list of todos in our application.#### State
Our StatsBloc
will have two states that it can be in:
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.Create blocs/stats/stats_state.dart
and let’s implement our StatsState
.
import 'package:equatable/equatable.dart';
import 'package:meta/meta.dart';
@immutable
abstract class StatsState extends Equatable {
StatsState([List props = const []]) : super(props);
}
class StatsLoading extends StatsState {
@override
String toString() => 'StatsLoading';
}
class StatsLoaded extends StatsState {
final int numActive;
final int numCompleted;
StatsLoaded(this.numActive, this.numCompleted)
: super([numActive, numCompleted]);
@override
String toString() {
return 'StatsLoaded { numActive: $numActive, numCompleted: $numCompleted }';
}
}
Next, let’s define and implement the StatsEvents
.
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.
import 'package:equatable/equatable.dart';
import 'package:meta/meta.dart';
import 'package:flutter_todos/models/models.dart';
@immutable
abstract class StatsEvent extends Equatable {
StatsEvent([List props = const []]) : super(props);
}
class UpdateStats extends StatsEvent {
final List<Todo> todos;
UpdateStats(this.todos) : super([todos]);
@override
String toString() => 'UpdateStats { todos: $todos }';
}
Now we’re ready to implement our StatsBloc
which will look very similar to the FilteredTodosBloc
.
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.
import 'dart:async';
import 'package:meta/meta.dart';
import 'package:bloc/bloc.dart';
import 'package:flutter_todos/blocs/blocs.dart';
class StatsBloc extends Bloc<StatsEvent, StatsState> {
final TodosBloc todosBloc;
StreamSubscription todosSubscription;
StatsBloc({@required this.todosBloc}) {
todosSubscription = todosBloc.state.listen((state) {
if (state is TodosLoaded) {
dispatch(UpdateStats(state.todos));
}
});
}
@override
StatsState get initialState => StatsLoading();
@override
Stream<StatsState> mapEventToState(
StatsState currentState,
StatsEvent event,
) async* {
if (event is UpdateStats) {
int numActive =
event.todos.where((todo) => !todo.complete).toList().length;
int numCompleted =
event.todos.where((todo) => todo.complete).toList().length;
yield StatsLoaded(numActive, numCompleted);
}
}
@override
void dispose() {
todosSubscription.cancel();
super.dispose();
}
}
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
.
Our
TodosBloc
will be responsible for convertingTodosEvents
intoTodosStates
and will manage the list of todos in our application.#### 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
:
enum AppTab { todos, stats }
Our TabBloc
will be responsible for handling a single TabEvent
:
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.Create blocs/tab/tab_event.dart
:
import 'package:equatable/equatable.dart';
import 'package:meta/meta.dart';
import 'package:flutter_todos/models/models.dart';
@immutable
abstract class TabEvent extends Equatable {
TabEvent([List props = const []]) : super(props);
}
class UpdateTab extends TabEvent {
final AppTab tab;
UpdateTab(this.tab) : super([tab]);
@override
String toString() => 'UpdateTab { tab: $tab }';
}
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.
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:flutter_todos/blocs/tab/tab.dart';
import 'package:flutter_todos/models/models.dart';
class TabBloc extends Bloc<TabEvent, AppTab> {
@override
AppTab get initialState => AppTab.todos;
@override
Stream<AppTab> mapEventToState(
AppTab currentState,
TabEvent event,
) async* {
if (event is UpdateTab) {
yield event.tab;
}
}
}
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.
Lastly, we’ll create another barrel file for our TabBloc
exports. Create blocs/tab/tab.dart
and export the two files:
export './tab_bloc.dart';
export './tab_event.dart';
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.
import 'package:bloc/bloc.dart';
class SimpleBlocDelegate extends BlocDelegate {
@override
void onTransition(Transition transition) {
print(transition);
}
@override
void onError(Object error, StackTrace stacktrace) {
print(error);
}
}
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…
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.
export './filtered_todos/filtered_todos.dart';
export './stats/stats.dart';
export './tab/tab.dart';
export './todos/todos.dart';
export './simple_bloc_delegate.dart';
Up next, we’ll focus on implementing the major screens in our Todos application.
Our
TodosBloc
will be responsible for convertingTodosEvents
intoTodosStates
and will manage the list of todos in our application.
Let’s create a new directory calledscreens
where we will put all of our new screen widgets and then createscreens/home_screen.dart
.
Our HomeScreen
will be a StatefulWidget
because it will need to create and dispose the TabBloc
, FilteredTodosBloc
, and StatsBloc
.
import 'package:flutter/material.dart';
import 'package:todos_app_core/todos_app_core.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_todos/blocs/blocs.dart';
import 'package:flutter_todos/widgets/widgets.dart';
import 'package:flutter_todos/localization.dart';
import 'package:flutter_todos/models/models.dart';
class HomeScreen extends StatefulWidget {
final void Function() onInit;
HomeScreen({@required this.onInit}) : super(key: ArchSampleKeys.homeScreen);
@override
_HomeScreenState createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
final TabBloc _tabBloc = TabBloc();
FilteredTodosBloc _filteredTodosBloc;
StatsBloc _statsBloc;
@override
void initState() {
widget.onInit();
_filteredTodosBloc = FilteredTodosBloc(
todosBloc: BlocProvider.of<TodosBloc>(context),
);
_statsBloc = StatsBloc(
todosBloc: BlocProvider.of<TodosBloc>(context),
);
super.initState();
}
@override
Widget build(BuildContext context) {
return BlocBuilder(
bloc: _tabBloc,
builder: (BuildContext context, AppTab activeTab) {
return BlocProviderTree(
blocProviders: [
BlocProvider<TabBloc>(bloc: _tabBloc),
BlocProvider<FilteredTodosBloc>(bloc: _filteredTodosBloc),
BlocProvider<StatsBloc>(bloc: _statsBloc),
],
child: Scaffold(
appBar: AppBar(
title: Text(FlutterBlocLocalizations.of(context).appTitle),
actions: [
FilterButton(visible: activeTab == AppTab.todos),
ExtraActions(),
],
),
body: activeTab == AppTab.todos ? FilteredTodos() : Stats(),
floatingActionButton: FloatingActionButton(
key: ArchSampleKeys.addTodoFab,
onPressed: () {
Navigator.pushNamed(context, ArchSampleRoutes.addTodo);
},
child: Icon(Icons.add),
tooltip: ArchSampleLocalizations.of(context).addTodo,
),
bottomNavigationBar: TabSelector(
activeTab: activeTab,
onTabSelected: (tab) => _tabBloc.dispatch(UpdateTab(tab)),
),
),
);
},
);
}
@override
void dispose() {
_statsBloc.dispose();
_filteredTodosBloc.dispose();
_tabBloc.dispose();
super.dispose();
}
}
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
.
Our
TodosBloc
will be responsible for convertingTodosEvents
intoTodosStates
and will manage the list of todos in our application.
Createscreens/details_screen.dart
and let’s build it.
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:todos_app_core/todos_app_core.dart';
import 'package:flutter_todos/blocs/todos/todos.dart';
import 'package:flutter_todos/screens/screens.dart';
import 'package:flutter_todos/flutter_todos_keys.dart';
class DetailsScreen extends StatelessWidget {
final String id;
DetailsScreen({Key key, @required this.id})
: super(key: key ?? ArchSampleKeys.todoDetailsScreen);
@override
Widget build(BuildContext context) {
final todosBloc = BlocProvider.of<TodosBloc>(context);
return BlocBuilder(
bloc: todosBloc,
builder: (BuildContext context, TodosState state) {
final todo = (state as TodosLoaded)
.todos
.firstWhere((todo) => todo.id == id, orElse: () => null);
final localizations = ArchSampleLocalizations.of(context);
return Scaffold(
appBar: AppBar(
title: Text(localizations.todoDetails),
actions: [
IconButton(
tooltip: localizations.deleteTodo,
key: ArchSampleKeys.deleteTodoButton,
icon: Icon(Icons.delete),
onPressed: () {
todosBloc.dispatch(DeleteTodo(todo));
Navigator.pop(context, todo);
},
)
],
),
body: todo == null
? Container(key: FlutterTodosKeys.emptyDetailsContainer)
: Padding(
padding: EdgeInsets.all(16.0),
child: ListView(
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: EdgeInsets.only(right: 8.0),
child: Checkbox(
key: FlutterTodosKeys.detailsScreenCheckBox,
value: todo.complete,
onChanged: (_) {
todosBloc.dispatch(
UpdateTodo(
todo.copyWith(complete: !todo.complete),
),
);
}),
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Hero(
tag: '${todo.id}__heroTag',
child: Container(
width: MediaQuery.of(context).size.width,
padding: EdgeInsets.only(
top: 8.0,
bottom: 16.0,
),
child: Text(
todo.task,
key: ArchSampleKeys.detailsTodoItemTask,
style:
Theme.of(context).textTheme.headline,
),
),
),
Text(
todo.note,
key: ArchSampleKeys.detailsTodoItemNote,
style: Theme.of(context).textTheme.subhead,
),
],
),
),
],
),
],
),
),
floatingActionButton: FloatingActionButton(
key: ArchSampleKeys.editTodoFab,
tooltip: localizations.editTodo,
child: Icon(Icons.edit),
onPressed: todo == null
? null
: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) {
return AddEditScreen(
key: ArchSampleKeys.editTodoScreen,
onSave: (task, note) {
todosBloc.dispatch(
UpdateTodo(
todo.copyWith(task: task, note: note),
),
);
},
isEditing: true,
todo: todo,
);
},
),
);
},
),
);
},
);
}
}
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.
Our
TodosBloc
will be responsible for convertingTodosEvents
intoTodosStates
and will manage the list of todos in our application.
Createscreens/add_edit_screen.dart
and let’s have a look at the implementation.
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:todos_app_core/todos_app_core.dart';
import 'package:flutter_todos/models/models.dart';
typedef OnSaveCallback = Function(String task, String note);
class AddEditScreen extends StatefulWidget {
final bool isEditing;
final OnSaveCallback onSave;
final Todo todo;
AddEditScreen({
Key key,
@required this.onSave,
@required this.isEditing,
this.todo,
}) : super(key: key ?? ArchSampleKeys.addTodoScreen);
@override
_AddEditScreenState createState() => _AddEditScreenState();
}
class _AddEditScreenState extends State<AddEditScreen> {
static final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
String _task;
String _note;
bool get isEditing => widget.isEditing;
@override
Widget build(BuildContext context) {
final localizations = ArchSampleLocalizations.of(context);
final textTheme = Theme.of(context).textTheme;
return Scaffold(
appBar: AppBar(
title: Text(
isEditing ? localizations.editTodo : localizations.addTodo,
),
),
body: Padding(
padding: EdgeInsets.all(16.0),
child: Form(
key: _formKey,
child: ListView(
children: [
TextFormField(
initialValue: isEditing ? widget.todo.task : '',
key: ArchSampleKeys.taskField,
autofocus: !isEditing,
style: textTheme.headline,
decoration: InputDecoration(
hintText: localizations.newTodoHint,
),
validator: (val) {
return val.trim().isEmpty
? localizations.emptyTodoError
: null;
},
onSaved: (value) => _task = value,
),
TextFormField(
initialValue: isEditing ? widget.todo.note : '',
key: ArchSampleKeys.noteField,
maxLines: 10,
style: textTheme.subhead,
decoration: InputDecoration(
hintText: localizations.notesHint,
),
onSaved: (value) => _note = value,
)
],
),
),
),
floatingActionButton: FloatingActionButton(
key:
isEditing ? ArchSampleKeys.saveTodoFab : ArchSampleKeys.saveNewTodo,
tooltip: isEditing ? localizations.saveChanges : localizations.addTodo,
child: Icon(isEditing ? Icons.check : Icons.add),
onPressed: () {
if (_formKey.currentState.validate()) {
_formKey.currentState.save();
widget.onSave(_task, _note);
Navigator.pop(context);
}
},
),
);
}
}
There’s nothing bloc-specific in this widget. It’s simply presenting a form and:
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.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.
Create screens/screens.dart
and export all three.
export './add_edit_screen.dart';
export './details_screen.dart';
export './home_screen.dart';
Our
TodosBloc
will be responsible for convertingTodosEvents
intoTodosStates
and will manage the list of todos in our application.
Let’s create a new directory calledwidgets
and put ourFilterButton
implementation inwidgets/filter_button.dart
.
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:todos_app_core/todos_app_core.dart';
import 'package:flutter_todos/blocs/filtered_todos/filtered_todos.dart';
import 'package:flutter_todos/models/models.dart';
class FilterButton extends StatelessWidget {
final bool visible;
FilterButton({this.visible, Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
final defaultStyle = Theme.of(context).textTheme.body1;
final activeStyle = Theme.of(context)
.textTheme
.body1
.copyWith(color: Theme.of(context).accentColor);
final FilteredTodosBloc filteredTodosBloc =
BlocProvider.of<FilteredTodosBloc>(context);
return BlocBuilder(
bloc: filteredTodosBloc,
builder: (BuildContext context, FilteredTodosState state) {
final button = _Button(
onSelected: (filter) {
filteredTodosBloc.dispatch(UpdateFilter(filter));
},
activeFilter: state is FilteredTodosLoaded
? state.activeFilter
: VisibilityFilter.all,
activeStyle: activeStyle,
defaultStyle: defaultStyle,
);
return AnimatedOpacity(
opacity: visible ? 1.0 : 0.0,
duration: Duration(milliseconds: 150),
child: visible ? button : IgnorePointer(child: button),
);
});
}
}
class _Button extends StatelessWidget {
const _Button({
Key key,
@required this.onSelected,
@required this.activeFilter,
@required this.activeStyle,
@required this.defaultStyle,
}) : super(key: key);
final PopupMenuItemSelected<VisibilityFilter> onSelected;
final VisibilityFilter activeFilter;
final TextStyle activeStyle;
final TextStyle defaultStyle;
@override
Widget build(BuildContext context) {
return PopupMenuButton<VisibilityFilter>(
key: ArchSampleKeys.filterButton,
tooltip: ArchSampleLocalizations.of(context).filterTodos,
onSelected: onSelected,
itemBuilder: (BuildContext context) => <PopupMenuItem<VisibilityFilter>>[
PopupMenuItem<VisibilityFilter>(
key: ArchSampleKeys.allFilter,
value: VisibilityFilter.all,
child: Text(
ArchSampleLocalizations.of(context).showAll,
style: activeFilter == VisibilityFilter.all
? activeStyle
: defaultStyle,
),
),
PopupMenuItem<VisibilityFilter>(
key: ArchSampleKeys.activeFilter,
value: VisibilityFilter.active,
child: Text(
ArchSampleLocalizations.of(context).showActive,
style: activeFilter == VisibilityFilter.active
? activeStyle
: defaultStyle,
),
),
PopupMenuItem<VisibilityFilter>(
key: ArchSampleKeys.completedFilter,
value: VisibilityFilter.completed,
child: Text(
ArchSampleLocalizations.of(context).showCompleted,
style: activeFilter == VisibilityFilter.completed
? activeStyle
: defaultStyle,
),
),
],
icon: Icon(Icons.filter_list),
);
}
}
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.
Our
TodosBloc
will be responsible for convertingTodosEvents
intoTodosStates
and will manage the list of todos in our application.
Since this widget doesn’t care about the filters it will interact with theTodosBloc
instead of theFilteredTodosBloc
.
Let’s create widgets/extra_actions.dart
and implement it.
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:todos_app_core/todos_app_core.dart';
import 'package:flutter_todos/blocs/todos/todos.dart';
import 'package:flutter_todos/models/models.dart';
import 'package:flutter_todos/flutter_todos_keys.dart';
class ExtraActions extends StatelessWidget {
ExtraActions({Key key}) : super(key: ArchSampleKeys.extraActionsButton);
@override
Widget build(BuildContext context) {
final todosBloc = BlocProvider.of<TodosBloc>(context);
return BlocBuilder(
bloc: todosBloc,
builder: (BuildContext context, TodosState state) {
if (state is TodosLoaded) {
bool allComplete = (todosBloc.currentState as TodosLoaded)
.todos
.every((todo) => todo.complete);
return PopupMenuButton<ExtraAction>(
key: FlutterTodosKeys.extraActionsPopupMenuButton,
onSelected: (action) {
switch (action) {
case ExtraAction.clearCompleted:
todosBloc.dispatch(ClearCompleted());
break;
case ExtraAction.toggleAllComplete:
todosBloc.dispatch(ToggleAll());
break;
}
},
itemBuilder: (BuildContext context) => <PopupMenuItem<ExtraAction>>[
PopupMenuItem<ExtraAction>(
key: ArchSampleKeys.toggleAll,
value: ExtraAction.toggleAllComplete,
child: Text(
allComplete
? ArchSampleLocalizations.of(context)
.markAllIncomplete
: ArchSampleLocalizations.of(context).markAllComplete,
),
),
PopupMenuItem<ExtraAction>(
key: ArchSampleKeys.clearCompleted,
value: ExtraAction.clearCompleted,
child: Text(
ArchSampleLocalizations.of(context).clearCompleted,
),
),
],
);
}
return Container(key: FlutterTodosKeys.extraActionsEmptyContainer);
},
);
}
}
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.
Our
TodosBloc
will be responsible for convertingTodosEvents
intoTodosStates
and will manage the list of todos in our application.
Let’s createwidgets/tab_selector.dart
and implement it.
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:todos_app_core/todos_app_core.dart';
import 'package:flutter_todos/models/models.dart';
class TabSelector extends StatelessWidget {
final AppTab activeTab;
final Function(AppTab) onTabSelected;
TabSelector({
Key key,
@required this.activeTab,
@required this.onTabSelected,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return BottomNavigationBar(
key: ArchSampleKeys.tabs,
currentIndex: AppTab.values.indexOf(activeTab),
onTap: (index) => onTabSelected(AppTab.values[index]),
items: AppTab.values.map((tab) {
return BottomNavigationBarItem(
icon: Icon(
tab == AppTab.todos ? Icons.list : Icons.show_chart,
key: tab == AppTab.todos
? ArchSampleKeys.todoTab
: ArchSampleKeys.statsTab,
),
title: Text(tab == AppTab.stats
? ArchSampleLocalizations.of(context).stats
: ArchSampleLocalizations.of(context).todos),
);
}).toList(),
);
}
}
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.
Our
TodosBloc
will be responsible for convertingTodosEvents
intoTodosStates
and will manage the list of todos in our application.
Createwidgets/filtered_todos.dart
and let’s implement it.
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:todos_app_core/todos_app_core.dart';
import 'package:flutter_todos/blocs/blocs.dart';
import 'package:flutter_todos/widgets/widgets.dart';
import 'package:flutter_todos/screens/screens.dart';
import 'package:flutter_todos/flutter_todos_keys.dart';
class FilteredTodos extends StatelessWidget {
FilteredTodos({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
final todosBloc = BlocProvider.of<TodosBloc>(context);
final filteredTodosBloc = BlocProvider.of<FilteredTodosBloc>(context);
final localizations = ArchSampleLocalizations.of(context);
return BlocBuilder(
bloc: filteredTodosBloc,
builder: (
BuildContext context,
FilteredTodosState state,
) {
if (state is FilteredTodosLoading) {
return LoadingIndicator(key: ArchSampleKeys.todosLoading);
} else if (state is FilteredTodosLoaded) {
final todos = state.filteredTodos;
return ListView.builder(
key: ArchSampleKeys.todoList,
itemCount: todos.length,
itemBuilder: (BuildContext context, int index) {
final todo = todos[index];
return TodoItem(
todo: todo,
onDismissed: (direction) {
todosBloc.dispatch(DeleteTodo(todo));
Scaffold.of(context).showSnackBar(DeleteTodoSnackBar(
key: ArchSampleKeys.snackbar,
todo: todo,
onUndo: () => todosBloc.dispatch(AddTodo(todo)),
localizations: localizations,
));
},
onTap: () async {
final removedTodo = await Navigator.of(context).push(
MaterialPageRoute(builder: (_) {
return DetailsScreen(id: todo.id);
}),
);
if (removedTodo != null) {
Scaffold.of(context).showSnackBar(DeleteTodoSnackBar(
key: ArchSampleKeys.snackbar,
todo: todo,
onUndo: () => todosBloc.dispatch(AddTodo(todo)),
localizations: localizations,
));
}
},
onCheckboxChanged: (_) {
todosBloc.dispatch(
UpdateTodo(todo.copyWith(complete: !todo.complete)),
);
},
);
},
);
} else {
return Container(key: FlutterTodosKeys.filteredTodosEmptyContainer);
}
},
);
}
}
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).
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.Our
TodosBloc
will be responsible for convertingTodosEvents
intoTodosStates
and will manage the list of todos in our application.
Createwidgets/todo_item.dart
and let’s build it.
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:todos_app_core/todos_app_core.dart';
import 'package:flutter_todos/models/models.dart';
class TodoItem extends StatelessWidget {
final DismissDirectionCallback onDismissed;
final GestureTapCallback onTap;
final ValueChanged<bool> onCheckboxChanged;
final Todo todo;
TodoItem({
Key key,
@required this.onDismissed,
@required this.onTap,
@required this.onCheckboxChanged,
@required this.todo,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Dismissible(
key: ArchSampleKeys.todoItem(todo.id),
onDismissed: onDismissed,
child: ListTile(
onTap: onTap,
leading: Checkbox(
key: ArchSampleKeys.todoItemCheckbox(todo.id),
value: todo.complete,
onChanged: onCheckboxChanged,
),
title: Hero(
tag: '${todo.id}__heroTag',
child: Container(
width: MediaQuery.of(context).size.width,
child: Text(
todo.task,
key: ArchSampleKeys.todoItemTask(todo.id),
style: Theme.of(context).textTheme.title,
),
),
),
subtitle: todo.note.isNotEmpty
? Text(
todo.note,
key: ArchSampleKeys.todoItemNote(todo.id),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.subhead,
)
: null,
),
);
}
}
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
.
Our
TodosBloc
will be responsible for convertingTodosEvents
intoTodosStates
and will manage the list of todos in our application.
Createwidgets/delete_todo_snack_bar.dart
and let’s implement it.
import 'package:flutter/material.dart';
import 'package:todos_app_core/todos_app_core.dart';
import 'package:flutter_todos/models/models.dart';
class DeleteTodoSnackBar extends SnackBar {
final ArchSampleLocalizations localizations;
DeleteTodoSnackBar({
Key key,
@required Todo todo,
@required VoidCallback onUndo,
@required this.localizations,
}) : super(
key: key,
content: Text(
localizations.todoDeleted(todo.task),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
duration: Duration(seconds: 2),
action: SnackBarAction(
label: localizations.undo,
onPressed: onUndo,
),
);
}
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!
Our
TodosBloc
will be responsible for convertingTodosEvents
intoTodosStates
and will manage the list of todos in our application.
Createwidgets/loading_indicator.dart
and let’s write it.
import 'package:flutter/material.dart';
class LoadingIndicator extends StatelessWidget {
LoadingIndicator({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Center(
child: CircularProgressIndicator(),
);
}
}
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.
Our
TodosBloc
will be responsible for convertingTodosEvents
intoTodosStates
and will manage the list of todos in our application.
Let’s createwidgets/stats.dart
and take a look at the implementation.
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:todos_app_core/todos_app_core.dart';
import 'package:flutter_todos/blocs/stats/stats.dart';
import 'package:flutter_todos/widgets/widgets.dart';
import 'package:flutter_todos/flutter_todos_keys.dart';
class Stats extends StatelessWidget {
Stats({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
final StatsBloc statsBloc = BlocProvider.of<StatsBloc>(context);
return BlocBuilder(
bloc: statsBloc,
builder: (BuildContext context, StatsState state) {
if (state is StatsLoading) {
return LoadingIndicator(key: FlutterTodosKeys.statsLoadingIndicator);
} else if (state is StatsLoaded) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Padding(
padding: EdgeInsets.only(bottom: 8.0),
child: Text(
ArchSampleLocalizations.of(context).completedTodos,
style: Theme.of(context).textTheme.title,
),
),
Padding(
padding: EdgeInsets.only(bottom: 24.0),
child: Text(
'${state.numCompleted}',
key: ArchSampleKeys.statsNumCompleted,
style: Theme.of(context).textTheme.subhead,
),
),
Padding(
padding: EdgeInsets.only(bottom: 8.0),
child: Text(
ArchSampleLocalizations.of(context).activeTodos,
style: Theme.of(context).textTheme.title,
),
),
Padding(
padding: EdgeInsets.only(bottom: 24.0),
child: Text(
"${state.numActive}",
key: ArchSampleKeys.statsNumActive,
style: Theme.of(context).textTheme.subhead,
),
)
],
),
);
} else {
return Container(key: FlutterTodosKeys.emptyStatsContainer);
}
},
);
}
}
We’re accessing the StatsBloc
using BlocProvider
and using BlocBuilder
to rebuild in response to state changes in the StatsBloc
state.
Let’s create main.dart
and our TodosApp
widget. We need to create a main
function and run our TodosApp
.
void main() {
BlocSupervisor().delegate = SimpleBlocDelegate();
runApp(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.
class TodosApp extends StatelessWidget {
final todosBloc = TodosBloc(
todosRepository: const TodosRepositoryFlutter(
fileStorage: const FileStorage(
'__flutter_bloc_app__',
getApplicationDocumentsDirectory,
),
),
);
@override
Widget build(BuildContext context) {
return BlocProvider(
bloc: todosBloc,
child: MaterialApp(
title: FlutterBlocLocalizations().appTitle,
theme: ArchSampleTheme.theme,
localizationsDelegates: [
ArchSampleLocalizationsDelegate(),
FlutterBlocLocalizationsDelegate(),
],
routes: {
ArchSampleRoutes.home: (context) {
return HomeScreen(
onInit: () => todosBloc.dispatch(LoadTodos()),
);
},
ArchSampleRoutes.addTodo: (context) {
return AddEditScreen(
key: ArchSampleKeys.addTodoScreen,
onSave: (task, note) {
todosBloc.dispatch(
AddTodo(Todo(task, note: note)),
);
},
isEditing: false,
);
},
},
),
);
}
}
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:
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.The entire main.dart
should look like this:
import 'package:flutter/material.dart';
import 'package:bloc/bloc.dart';
import 'package:path_provider/path_provider.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:todos_repository_simple/todos_repository_simple.dart';
import 'package:todos_app_core/todos_app_core.dart';
import 'package:flutter_todos/localization.dart';
import 'package:flutter_todos/blocs/blocs.dart';
import 'package:flutter_todos/models/models.dart';
import 'package:flutter_todos/screens/screens.dart';
void main() {
BlocSupervisor().delegate = SimpleBlocDelegate();
runApp(TodosApp());
}
class TodosApp extends StatelessWidget {
final todosBloc = TodosBloc(
todosRepository: const TodosRepositoryFlutter(
fileStorage: const FileStorage(
'__flutter_bloc_app__',
getApplicationDocumentsDirectory,
),
),
);
@override
Widget build(BuildContext context) {
return BlocProvider(
bloc: todosBloc,
child: MaterialApp(
title: FlutterBlocLocalizations().appTitle,
theme: ArchSampleTheme.theme,
localizationsDelegates: [
ArchSampleLocalizationsDelegate(),
FlutterBlocLocalizationsDelegate(),
],
routes: {
ArchSampleRoutes.home: (context) {
return HomeScreen(
onInit: () => todosBloc.dispatch(LoadTodos()),
);
},
ArchSampleRoutes.addTodo: (context) {
return AddEditScreen(
key: ArchSampleKeys.addTodoScreen,
onSave: (task, note) {
todosBloc.dispatch(
AddTodo(Todo(task, note: note)),
);
},
isEditing: false,
);
},
},
),
);
}
}
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.
#flutter #mobile-apps #ios #android