1565249903
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.20.0
flutter:
sdk: flutterdependency_overrides:
todos_app_core:
git:
url: https://github.com/felangel/flutter_architecture_samples
path: todos_app_core
ref: expose-repositories
todos_repository_core:
git:
url: https://github.com/felangel/flutter_architecture_samples
path: todos_repository_core
ref: expose-repositories
todos_repository_simple:
git:
url: https://github.com/felangel/flutter_architecture_samples
path: todos_repository_simple
ref: expose-repositories
flutter:
uses-material-design: true
and then install all of the dependencies
flutter packages get
Before we jump into the application code, let’s create flutter_todos_keys.dart
. This file will contain keys which we will use to uniquely identify important widgets. We can later write tests that find widgets based on keys.
import ‘package:flutter/widgets.dart’;class FlutterTodosKeys {
static final extraActionsPopupMenuButton =
const Key(‘extraActionsPopupMenuButton’);
static final extraActionsEmptyContainer =
const Key(‘extraActionsEmptyContainer’);
static final filteredTodosEmptyContainer =
const Key(‘filteredTodosEmptyContainer’);
static final statsLoadingIndicator = const Key(‘statsLoadingIndicator’);
static final emptyStatsContainer = const Key(‘emptyStatsContainer’);
static final emptyDetailsContainer = const Key(‘emptyDetailsContainer’);
static final detailsScreenCheckBox = const Key(‘detailsScreenCheckBox’);
}
We will reference these keys throughout the rest of the tutorial.
Localization
One last concept that we will touch on before going into the application itself is localization. Create localization.dart and we’ll create the foundation for multi-language support.
import ‘dart:async’;import ‘package:flutter/material.dart’;
class FlutterBlocLocalizations {
static FlutterBlocLocalizations of(BuildContext context) {
return Localizations.of<FlutterBlocLocalizations>(
context,
FlutterBlocLocalizations,
);
}String get appTitle => “Flutter Todos”;
}class FlutterBlocLocalizationsDelegate
extends LocalizationsDelegate<FlutterBlocLocalizations> {
@override
Future<FlutterBlocLocalizations> load(Locale locale) =>
Future(() => FlutterBlocLocalizations());@override
bool shouldReload(FlutterBlocLocalizationsDelegate old) => false;@override
bool isSupported(Locale locale) =>
locale.languageCode.toLowerCase().contains(“en”);
}
We can now import and provide our FlutterBlocLocalizationsDelegate to our MaterialApp (later in this tutorial).
For more information on localization check out the official flutter docs.
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 converting TodosEvents into TodosStates and will manage the list of todos.
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.
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:
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.
Events
The events we will need to handle in our TodosBloc are:
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.
Bloc
Let’s create blocs/todos/todos_bloc.dart and get started! We just need to implement initialState and mapEventToState.
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(TodosEvent event) async* {
if (event is LoadTodos) {
yield* _mapLoadTodosToState();
} else if (event is AddTodo) {
yield* _mapAddTodoToState(event);
} else if (event is UpdateTodo) {
yield* _mapUpdateTodoToState(event);
} else if (event is DeleteTodo) {
yield* _mapDeleteTodoToState(event);
} else if (event is ToggleAll) {
yield* _mapToggleAllToState();
} else if (event is ClearCompleted) {
yield* _mapClearCompletedToState();
}
}Stream<TodosState> mapLoadTodosToState() async* {
try {
final todos = await this.todosRepository.loadTodos();
yield TodosLoaded(
todos.map(Todo.fromEntity).toList(),
);
} catch () {
yield TodosNotLoaded();
}
}Stream<TodosState> _mapAddTodoToState(AddTodo event) async* {
if (currentState is TodosLoaded) {
final List<Todo> updatedTodos =
List.from((currentState as TodosLoaded).todos)…add(event.todo);
yield TodosLoaded(updatedTodos);
_saveTodos(updatedTodos);
}
}Stream<TodosState> _mapUpdateTodoToState(UpdateTodo event) async* {
if (currentState is TodosLoaded) {
final List<Todo> updatedTodos =
(currentState as TodosLoaded).todos.map((todo) {
return todo.id == event.updatedTodo.id ? event.updatedTodo : todo;
}).toList();
yield TodosLoaded(updatedTodos);
_saveTodos(updatedTodos);
}
}Stream<TodosState> _mapDeleteTodoToState(DeleteTodo event) async* {
if (currentState is TodosLoaded) {
final updatedTodos = (currentState as TodosLoaded)
.todos
.where((todo) => todo.id != event.todo.id)
.toList();
yield TodosLoaded(updatedTodos);
_saveTodos(updatedTodos);
}
}Stream<TodosState> _mapToggleAllToState() async* {
if (currentState is TodosLoaded) {
final allComplete =
(currentState as TodosLoaded).todos.every((todo) => todo.complete);
final List<Todo> updatedTodos = (currentState as TodosLoaded)
.todos
.map((todo) => todo.copyWith(complete: !allComplete))
.toList();
yield TodosLoaded(updatedTodos);
_saveTodos(updatedTodos);
}
}Stream<TodosState> _mapClearCompletedToState() async* {
if (currentState is TodosLoaded) {
final List<Todo> updatedTodos = (currentState as TodosLoaded)
.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’;
Filtered Todos Bloc
The FilteredTodosBloc will be responsible for reacting to state changes in the TodosBloc 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:
We can create models/visibility_filter.dart and define our filter as an enum:
enum VisibilityFilter { all, active, completed }
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:
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.
Events
We’re going to implement two events for our FilteredTodosBloc:
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!
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.
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(FilteredTodosEvent event) async* {
if (event is UpdateFilter) {
yield* _mapUpdateFilterToState(event);
} else if (event is UpdateTodos) {
yield* _mapTodosUpdatedToState(event);
}
}Stream<FilteredTodosState> _mapUpdateFilterToState(
UpdateFilter event,
) async* {
if (todosBloc.currentState is TodosLoaded) {
yield FilteredTodosLoaded(
_mapTodosToFilteredTodos(
(todosBloc.currentState as TodosLoaded).todos,
event.filter,
),
event.filter,
);
}
}Stream<FilteredTodosState> _mapTodosUpdatedToState(
UpdateTodos event,
) async* {
final visibilityFilter = currentState is FilteredTodosLoaded
? (currentState as FilteredTodosLoaded).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’;
Next, we’re going to implement the StatsBloc.
The StatsBloc will 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 TodosBloc itself so that it can react to changes in the TodosBloc state.
State
Our StatsBloc will have two states that it can be in:
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.
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.
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.
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.
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(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.
Tab Bloc
The TabBloc will be responsible for maintaining the state of the tabs in our application. It will be taking TabEvents as input and outputting AppTabs.
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 }
Event
Our TabBloc will be responsible for handling a single TabEvent:
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 }’;
}
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.
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(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.
Barrel File
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 onEvent(Bloc bloc, Object event) {
super.onEvent(bloc, event);
print(event);
}@override
void onTransition(Bloc bloc, Transition transition) {
super.onTransition(bloc, transition);
print(transition);
}@override
void onError(Bloc bloc, Object error, StackTrace stacktrace) {
super.onError(bloc, error, 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. You can hook up your BlocDelegate to google analytics, sentry, crashlytics, etc…
Blocs Barrel
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.
Home Screen
Our HomeScreen will be responsible for creating the Scaffold of our application. It will maintain the AppBar, BottomNavigationBar, as well as the Stats/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.
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 StatelessWidget {
@override
Widget build(BuildContext context) {
final tabBloc = BlocProvider.of<TabBloc>(context);
return BlocBuilder<TabBloc, AppTab>(
builder: (context, activeTab) {
return 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)),
),
);
},
);
}
}
The HomeScreen accesses the TabBloc using BlocProvider.of<TabBloc>(context) which will be made available from our root TodosApp widget (we’ll get to it later in this tutorial).
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.
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<TodosBloc, TodosState>(
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.
Add/Edit Screen
The AddEditScreen widget allows the user to either create a new todo or update an existing todo based on the isEditing flag that is passed via the constructor.
Create screens/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:
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
Create screens/screens.dart and export all three.
export ‘./add_edit_screen.dart’;
export ‘./details_screen.dart’;
export ‘./home_screen.dart’;
Next, let’s implement all of the “widgets” (anything that isn’t a screen).
Filter Button
The FilterButton widget will be responsible for providing the user with a list of filter options and will notify the FilteredTodosBloc when a new filter is selected.
Let’s create a new directory called widgets and put our FilterButton implementation in widgets/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<FilteredTodosBloc, FilteredTodosState>(
builder: (context, 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.
Extra Actions
Similarly to the FilterButton, the ExtraActions 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 the ExtraAction model in models/extra_action.dart.
enum ExtraAction { toggleAllComplete, clearCompleted }
And don’t forget to export it from the models/models.dart barrel file.
Next, 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<TodosBloc, TodosState>(
builder: (context, 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.
Tab Selector
The TabSelector widget is responsible for displaying the tabs in the BottomNavigationBar and handling user input.
Let’s create widgets/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.
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.
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 localizations = ArchSampleLocalizations.of(context);return BlocBuilder<FilteredTodosBloc, FilteredTodosState>(
builder: (context, 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).
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.
From the FilteredTodos widget, the user can navigate to the DetailsScreen where it is possible to edit or delete the selected todo. Since our FilteredTodos widget renders a list of TodoItem widgets, we’ll take a look at those next.
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.
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.
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.
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!
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.
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.
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.
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) {
return BlocBuilder<StatsBloc, StatsState>(
builder: (context, 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.
Putting it all together
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(
BlocProvider(
builder: (context) {
return TodosBloc(
todosRepository: const TodosRepositoryFlutter(
fileStorage: const FileStorage(
‘flutter_bloc_app’,
getApplicationDocumentsDirectory,
),
),
)…dispatch(LoadTodos());
},
child: 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.
Note: We are also wrapping our TodosApp widget in a BlocProvider which manages initializing, disposing, and providing the TodosBloc to our entire widget tree from flutter_bloc. We immediately dispatch the LoadTodos event in order to request the latest todos.
Next, let’s implement our TodosApp widget.
class TodosApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
final todosBloc = BlocProvider.of<TodosBloc>(context);
return MaterialApp(
title: FlutterBlocLocalizations().appTitle,
theme: ArchSampleTheme.theme,
localizationsDelegates: [
ArchSampleLocalizationsDelegate(),
FlutterBlocLocalizationsDelegate(),
],
routes: {
ArchSampleRoutes.home: (context) {
return MultiBlocProvider(
providers: [
BlocProvider<TabBloc>(
builder: (context) => TabBloc(),
),
BlocProvider<FilteredTodosBloc>(
builder: (context) => FilteredTodosBloc(todosBloc: todosBloc),
),
BlocProvider<StatsBloc>(
builder: (context) => StatsBloc(todosBloc: todosBloc),
),
],
child: HomeScreen(),
);
},
ArchSampleRoutes.addTodo: (context) {
return AddEditScreen(
key: ArchSampleKeys.addTodoScreen,
onSave: (task, note) {
todosBloc.dispatch(
AddTodo(Todo(task, note: note)),
);
},
isEditing: false,
);
},
},
);
}
}
Our TodosApp is a StatelessWidget which accesses the provided TodosBloc via the BuildContext.
The TodosApp has two routes:
The TodosApp also makes the TabBloc, FilteredTodosBloc, and StatsBloc available to the widgets in its subtree by using the MultiBlocProvider widget from flutter_bloc.
MultiBlocProvider(
providers: [
BlocProvider<TabBloc>(
builder: (context) => TabBloc(),
),
BlocProvider<FilteredTodosBloc>(
builder: (context) => FilteredTodosBloc(todosBloc: todosBloc),
),
BlocProvider<StatsBloc>(
builder: (context) => StatsBloc(todosBloc: todosBloc),
),
],
child: HomeScreen(),
);
is equivalent to writing
BlocProvider<TabBloc>(
builder: (context) => TabBloc(),
child: BlocProvider<FilteredTodosBloc>(
builder: (context) => FilteredTodosBloc(todosBloc: todosBloc),
child: BlocProvider<StatsBloc>(
builder: (context) => StatsBloc(todosBloc: todosBloc),
child: Scaffold(…),
),
),
);
You can see how using MultiBlocProvider helps reduce the levels of nesting and makes the code easier to read and maintain.
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 oversees Blocs and delegates to BlocDelegate.
// We can set the BlocSupervisor’s delegate to an instance ofSimpleBlocDelegate
.
// This will allow us to handle all transitions and errors in SimpleBlocDelegate.
BlocSupervisor.delegate = SimpleBlocDelegate();
runApp(
BlocProvider(
builder: (context) {
return TodosBloc(
todosRepository: const TodosRepositoryFlutter(
fileStorage: const FileStorage(
‘flutter_bloc_app’,
getApplicationDocumentsDirectory,
),
),
)…dispatch(LoadTodos());
},
child: TodosApp(),
),
);
}class TodosApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
final todosBloc = BlocProvider.of<TodosBloc>(context);
return MaterialApp(
title: FlutterBlocLocalizations().appTitle,
theme: ArchSampleTheme.theme,
localizationsDelegates: [
ArchSampleLocalizationsDelegate(),
FlutterBlocLocalizationsDelegate(),
],
routes: {
ArchSampleRoutes.home: (context) {
return MultiBlocProvider(
providers: [
BlocProvider<TabBloc>(
builder: (context) => TabBloc(),
),
BlocProvider<FilteredTodosBloc>(
builder: (context) => FilteredTodosBloc(todosBloc: todosBloc),
),
BlocProvider<StatsBloc>(
builder: (context) => StatsBloc(todosBloc: todosBloc),
),
],
child: HomeScreen(),
);
},
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 successfully 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.
Thanks for reading ❤
If you liked this post, share it with all of your programming buddies!
Follow us on Facebook | Twitter
☞ Learn Flutter & Dart to Build iOS & Android Apps
☞ Flutter & Dart - The Complete Flutter App Development Course
☞ Dart and Flutter: The Complete Developer’s Guide
☞ Flutter Tutorial - Flight List UI Example In Flutter
☞ Flutter Tutorial for Beginners - Full Tutorial
☞ A Beginners Guide to the Flutter Bottom Sheet
☞ Flutter Course - Full Tutorial for Beginners (Build iOS and Android Apps)
☞ Flutter Tutorial For Beginners - Build Your First Flutter App
☞ Building the SwiftUI Sample App in Flutter
☞ Building Cryptocurrency Pricing App with Flutter
#flutter #mobile-apps
1597014000
Flutter Google cross-platform UI framework has released a new version 1.20 stable.
Flutter is Google’s UI framework to make apps for Android, iOS, Web, Windows, Mac, Linux, and Fuchsia OS. Since the last 2 years, the flutter Framework has already achieved popularity among mobile developers to develop Android and iOS apps. In the last few releases, Flutter also added the support of making web applications and desktop applications.
Last month they introduced the support of the Linux desktop app that can be distributed through Canonical Snap Store(Snapcraft), this enables the developers to publish there Linux desktop app for their users and publish on Snap Store. If you want to learn how to Publish Flutter Desktop app in Snap Store that here is the tutorial.
Flutter 1.20 Framework is built on Google’s made Dart programming language that is a cross-platform language providing native performance, new UI widgets, and other more features for the developer usage.
Here are the few key points of this release:
In this release, they have got multiple performance improvements in the Dart language itself. A new improvement is to reduce the app size in the release versions of the app. Another performance improvement is to reduce junk in the display of app animation by using the warm-up phase.
If your app is junk information during the first run then the Skia Shading Language shader provides for pre-compilation as part of your app’s build. This can speed it up by more than 2x.
Added a better support of mouse cursors for web and desktop flutter app,. Now many widgets will show cursor on top of them or you can specify the type of supported cursor you want.
Autofill was already supported in native applications now its been added to the Flutter SDK. Now prefilled information stored by your OS can be used for autofill in the application. This feature will be available soon on the flutter web.
A new widget for interaction
InteractiveViewer
is a new widget design for common interactions in your app like pan, zoom drag and drop for resizing the widget. Informations on this you can check more on this API documentation where you can try this widget on the DartPad. In this release, drag-drop has more features added like you can know precisely where the drop happened and get the position.
In this new release, there are many pre-existing widgets that were updated to match the latest material guidelines, these updates include better interaction with Slider
and RangeSlider
, DatePicker
with support for date range and time picker with the new style.
pubspec.yaml
formatOther than these widget updates there is some update within the project also like in pubspec.yaml
file format. If you are a flutter plugin publisher then your old pubspec.yaml
is no longer supported to publish a plugin as the older format does not specify for which platform plugin you are making. All existing plugin will continue to work with flutter apps but you should make a plugin update as soon as possible.
Visual Studio code flutter extension got an update in this release. You get a preview of new features where you can analyze that Dev tools in your coding workspace. Enable this feature in your vs code by _dart.previewEmbeddedDevTools_
setting. Dart DevTools menu you can choose your favorite page embed on your code workspace.
The updated the Dev tools comes with the network page that enables network profiling. You can track the timings and other information like status and content type of your** network calls** within your app. You can also monitor gRPC traffic.
Pigeon is a command-line tool that will generate types of safe platform channels without adding additional dependencies. With this instead of manually matching method strings on platform channel and serializing arguments, you can invoke native class and pass nonprimitive data objects by directly calling the Dart
method.
There is still a long list of updates in the new version of Flutter 1.2 that we cannot cover in this blog. You can get more details you can visit the official site to know more. Also, you can subscribe to the Navoki newsletter to get updates on these features and upcoming new updates and lessons. In upcoming new versions, we might see more new features and improvements.
You can get more free Flutter tutorials you can follow these courses:
#dart #developers #flutter #app developed #dart devtools in visual studio code #firebase local emulator suite in flutter #flutter autofill #flutter date picker #flutter desktop linux app build and publish on snapcraft store #flutter pigeon #flutter range slider #flutter slider #flutter time picker #flutter tutorial #flutter widget #google flutter #linux #navoki #pubspec format #setup flutter desktop on windows
1667425440
Perl script converts PDF files to Gerber format
Pdf2Gerb generates Gerber 274X photoplotting and Excellon drill files from PDFs of a PCB. Up to three PDFs are used: the top copper layer, the bottom copper layer (for 2-sided PCBs), and an optional silk screen layer. The PDFs can be created directly from any PDF drawing software, or a PDF print driver can be used to capture the Print output if the drawing software does not directly support output to PDF.
The general workflow is as follows:
Please note that Pdf2Gerb does NOT perform DRC (Design Rule Checks), as these will vary according to individual PCB manufacturer conventions and capabilities. Also note that Pdf2Gerb is not perfect, so the output files must always be checked before submitting them. As of version 1.6, Pdf2Gerb supports most PCB elements, such as round and square pads, round holes, traces, SMD pads, ground planes, no-fill areas, and panelization. However, because it interprets the graphical output of a Print function, there are limitations in what it can recognize (or there may be bugs).
See docs/Pdf2Gerb.pdf for install/setup, config, usage, and other info.
#Pdf2Gerb config settings:
#Put this file in same folder/directory as pdf2gerb.pl itself (global settings),
#or copy to another folder/directory with PDFs if you want PCB-specific settings.
#There is only one user of this file, so we don't need a custom package or namespace.
#NOTE: all constants defined in here will be added to main namespace.
#package pdf2gerb_cfg;
use strict; #trap undef vars (easier debug)
use warnings; #other useful info (easier debug)
##############################################################################################
#configurable settings:
#change values here instead of in main pfg2gerb.pl file
use constant WANT_COLORS => ($^O !~ m/Win/); #ANSI colors no worky on Windows? this must be set < first DebugPrint() call
#just a little warning; set realistic expectations:
#DebugPrint("${\(CYAN)}Pdf2Gerb.pl ${\(VERSION)}, $^O O/S\n${\(YELLOW)}${\(BOLD)}${\(ITALIC)}This is EXPERIMENTAL software. \nGerber files MAY CONTAIN ERRORS. Please CHECK them before fabrication!${\(RESET)}", 0); #if WANT_DEBUG
use constant METRIC => FALSE; #set to TRUE for metric units (only affect final numbers in output files, not internal arithmetic)
use constant APERTURE_LIMIT => 0; #34; #max #apertures to use; generate warnings if too many apertures are used (0 to not check)
use constant DRILL_FMT => '2.4'; #'2.3'; #'2.4' is the default for PCB fab; change to '2.3' for CNC
use constant WANT_DEBUG => 0; #10; #level of debug wanted; higher == more, lower == less, 0 == none
use constant GERBER_DEBUG => 0; #level of debug to include in Gerber file; DON'T USE FOR FABRICATION
use constant WANT_STREAMS => FALSE; #TRUE; #save decompressed streams to files (for debug)
use constant WANT_ALLINPUT => FALSE; #TRUE; #save entire input stream (for debug ONLY)
#DebugPrint(sprintf("${\(CYAN)}DEBUG: stdout %d, gerber %d, want streams? %d, all input? %d, O/S: $^O, Perl: $]${\(RESET)}\n", WANT_DEBUG, GERBER_DEBUG, WANT_STREAMS, WANT_ALLINPUT), 1);
#DebugPrint(sprintf("max int = %d, min int = %d\n", MAXINT, MININT), 1);
#define standard trace and pad sizes to reduce scaling or PDF rendering errors:
#This avoids weird aperture settings and replaces them with more standardized values.
#(I'm not sure how photoplotters handle strange sizes).
#Fewer choices here gives more accurate mapping in the final Gerber files.
#units are in inches
use constant TOOL_SIZES => #add more as desired
(
#round or square pads (> 0) and drills (< 0):
.010, -.001, #tiny pads for SMD; dummy drill size (too small for practical use, but needed so StandardTool will use this entry)
.031, -.014, #used for vias
.041, -.020, #smallest non-filled plated hole
.051, -.025,
.056, -.029, #useful for IC pins
.070, -.033,
.075, -.040, #heavier leads
# .090, -.043, #NOTE: 600 dpi is not high enough resolution to reliably distinguish between .043" and .046", so choose 1 of the 2 here
.100, -.046,
.115, -.052,
.130, -.061,
.140, -.067,
.150, -.079,
.175, -.088,
.190, -.093,
.200, -.100,
.220, -.110,
.160, -.125, #useful for mounting holes
#some additional pad sizes without holes (repeat a previous hole size if you just want the pad size):
.090, -.040, #want a .090 pad option, but use dummy hole size
.065, -.040, #.065 x .065 rect pad
.035, -.040, #.035 x .065 rect pad
#traces:
.001, #too thin for real traces; use only for board outlines
.006, #minimum real trace width; mainly used for text
.008, #mainly used for mid-sized text, not traces
.010, #minimum recommended trace width for low-current signals
.012,
.015, #moderate low-voltage current
.020, #heavier trace for power, ground (even if a lighter one is adequate)
.025,
.030, #heavy-current traces; be careful with these ones!
.040,
.050,
.060,
.080,
.100,
.120,
);
#Areas larger than the values below will be filled with parallel lines:
#This cuts down on the number of aperture sizes used.
#Set to 0 to always use an aperture or drill, regardless of size.
use constant { MAX_APERTURE => max((TOOL_SIZES)) + .004, MAX_DRILL => -min((TOOL_SIZES)) + .004 }; #max aperture and drill sizes (plus a little tolerance)
#DebugPrint(sprintf("using %d standard tool sizes: %s, max aper %.3f, max drill %.3f\n", scalar((TOOL_SIZES)), join(", ", (TOOL_SIZES)), MAX_APERTURE, MAX_DRILL), 1);
#NOTE: Compare the PDF to the original CAD file to check the accuracy of the PDF rendering and parsing!
#for example, the CAD software I used generated the following circles for holes:
#CAD hole size: parsed PDF diameter: error:
# .014 .016 +.002
# .020 .02267 +.00267
# .025 .026 +.001
# .029 .03167 +.00267
# .033 .036 +.003
# .040 .04267 +.00267
#This was usually ~ .002" - .003" too big compared to the hole as displayed in the CAD software.
#To compensate for PDF rendering errors (either during CAD Print function or PDF parsing logic), adjust the values below as needed.
#units are pixels; for example, a value of 2.4 at 600 dpi = .0004 inch, 2 at 600 dpi = .0033"
use constant
{
HOLE_ADJUST => -0.004 * 600, #-2.6, #holes seemed to be slightly oversized (by .002" - .004"), so shrink them a little
RNDPAD_ADJUST => -0.003 * 600, #-2, #-2.4, #round pads seemed to be slightly oversized, so shrink them a little
SQRPAD_ADJUST => +0.001 * 600, #+.5, #square pads are sometimes too small by .00067, so bump them up a little
RECTPAD_ADJUST => 0, #(pixels) rectangular pads seem to be okay? (not tested much)
TRACE_ADJUST => 0, #(pixels) traces seemed to be okay?
REDUCE_TOLERANCE => .001, #(inches) allow this much variation when reducing circles and rects
};
#Also, my CAD's Print function or the PDF print driver I used was a little off for circles, so define some additional adjustment values here:
#Values are added to X/Y coordinates; units are pixels; for example, a value of 1 at 600 dpi would be ~= .002 inch
use constant
{
CIRCLE_ADJUST_MINX => 0,
CIRCLE_ADJUST_MINY => -0.001 * 600, #-1, #circles were a little too high, so nudge them a little lower
CIRCLE_ADJUST_MAXX => +0.001 * 600, #+1, #circles were a little too far to the left, so nudge them a little to the right
CIRCLE_ADJUST_MAXY => 0,
SUBST_CIRCLE_CLIPRECT => FALSE, #generate circle and substitute for clip rects (to compensate for the way some CAD software draws circles)
WANT_CLIPRECT => TRUE, #FALSE, #AI doesn't need clip rect at all? should be on normally?
RECT_COMPLETION => FALSE, #TRUE, #fill in 4th side of rect when 3 sides found
};
#allow .012 clearance around pads for solder mask:
#This value effectively adjusts pad sizes in the TOOL_SIZES list above (only for solder mask layers).
use constant SOLDER_MARGIN => +.012; #units are inches
#line join/cap styles:
use constant
{
CAP_NONE => 0, #butt (none); line is exact length
CAP_ROUND => 1, #round cap/join; line overhangs by a semi-circle at either end
CAP_SQUARE => 2, #square cap/join; line overhangs by a half square on either end
CAP_OVERRIDE => FALSE, #cap style overrides drawing logic
};
#number of elements in each shape type:
use constant
{
RECT_SHAPELEN => 6, #x0, y0, x1, y1, count, "rect" (start, end corners)
LINE_SHAPELEN => 6, #x0, y0, x1, y1, count, "line" (line seg)
CURVE_SHAPELEN => 10, #xstart, ystart, x0, y0, x1, y1, xend, yend, count, "curve" (bezier 2 points)
CIRCLE_SHAPELEN => 5, #x, y, 5, count, "circle" (center + radius)
};
#const my %SHAPELEN =
#Readonly my %SHAPELEN =>
our %SHAPELEN =
(
rect => RECT_SHAPELEN,
line => LINE_SHAPELEN,
curve => CURVE_SHAPELEN,
circle => CIRCLE_SHAPELEN,
);
#panelization:
#This will repeat the entire body the number of times indicated along the X or Y axes (files grow accordingly).
#Display elements that overhang PCB boundary can be squashed or left as-is (typically text or other silk screen markings).
#Set "overhangs" TRUE to allow overhangs, FALSE to truncate them.
#xpad and ypad allow margins to be added around outer edge of panelized PCB.
use constant PANELIZE => {'x' => 1, 'y' => 1, 'xpad' => 0, 'ypad' => 0, 'overhangs' => TRUE}; #number of times to repeat in X and Y directions
# Set this to 1 if you need TurboCAD support.
#$turboCAD = FALSE; #is this still needed as an option?
#CIRCAD pad generation uses an appropriate aperture, then moves it (stroke) "a little" - we use this to find pads and distinguish them from PCB holes.
use constant PAD_STROKE => 0.3; #0.0005 * 600; #units are pixels
#convert very short traces to pads or holes:
use constant TRACE_MINLEN => .001; #units are inches
#use constant ALWAYS_XY => TRUE; #FALSE; #force XY even if X or Y doesn't change; NOTE: needs to be TRUE for all pads to show in FlatCAM and ViewPlot
use constant REMOVE_POLARITY => FALSE; #TRUE; #set to remove subtractive (negative) polarity; NOTE: must be FALSE for ground planes
#PDF uses "points", each point = 1/72 inch
#combined with a PDF scale factor of .12, this gives 600 dpi resolution (1/72 * .12 = 600 dpi)
use constant INCHES_PER_POINT => 1/72; #0.0138888889; #multiply point-size by this to get inches
# The precision used when computing a bezier curve. Higher numbers are more precise but slower (and generate larger files).
#$bezierPrecision = 100;
use constant BEZIER_PRECISION => 36; #100; #use const; reduced for faster rendering (mainly used for silk screen and thermal pads)
# Ground planes and silk screen or larger copper rectangles or circles are filled line-by-line using this resolution.
use constant FILL_WIDTH => .01; #fill at most 0.01 inch at a time
# The max number of characters to read into memory
use constant MAX_BYTES => 10 * M; #bumped up to 10 MB, use const
use constant DUP_DRILL1 => TRUE; #FALSE; #kludge: ViewPlot doesn't load drill files that are too small so duplicate first tool
my $runtime = time(); #Time::HiRes::gettimeofday(); #measure my execution time
print STDERR "Loaded config settings from '${\(__FILE__)}'.\n";
1; #last value must be truthful to indicate successful load
#############################################################################################
#junk/experiment:
#use Package::Constants;
#use Exporter qw(import); #https://perldoc.perl.org/Exporter.html
#my $caller = "pdf2gerb::";
#sub cfg
#{
# my $proto = shift;
# my $class = ref($proto) || $proto;
# my $settings =
# {
# $WANT_DEBUG => 990, #10; #level of debug wanted; higher == more, lower == less, 0 == none
# };
# bless($settings, $class);
# return $settings;
#}
#use constant HELLO => "hi there2"; #"main::HELLO" => "hi there";
#use constant GOODBYE => 14; #"main::GOODBYE" => 12;
#print STDERR "read cfg file\n";
#our @EXPORT_OK = Package::Constants->list(__PACKAGE__); #https://www.perlmonks.org/?node_id=1072691; NOTE: "_OK" skips short/common names
#print STDERR scalar(@EXPORT_OK) . " consts exported:\n";
#foreach(@EXPORT_OK) { print STDERR "$_\n"; }
#my $val = main::thing("xyz");
#print STDERR "caller gave me $val\n";
#foreach my $arg (@ARGV) { print STDERR "arg $arg\n"; }
Author: swannman
Source Code: https://github.com/swannman/pdf2gerb
License: GPL-3.0 license
1622532470
Hire Flutter App Developers: WebClues Infotech is a Flutter App Development company. Our Flutter mobile app development team can create cross-platform apps for different industry verticals. Our Flutter developers will help you extend your business’s scope by developing enhanced functionality and a feature-rich app. To provide a rich user experience to your users, hire dedicated Flutter app developers from WebClues Infotech today!
#hire flutter app developers #hire dedicated flutter app developer usa #hire flutter app developer usa #hire dedicated flutter app developer #hire flutter developer #flutter app development company
1606986883
Are you looking for the best flutter app development company? Then AppClues Infotech is the leading flutter app development company in USA offering the best service worldwide. We focused on developing hybrid mobile apps on Android & iOS and assures our end-user about exceptional and functionally-rich mobile apps.
For more info:
Website: https://www.appcluesinfotech.com/
Email: info@appcluesinfotech.com
Call: +1-978-309-9910
#top flutter app development company in usa #best flutter app development service #best flutter app development company #hire flutter app developers #flutter app development company #expert flutter app development company
1608627556
AppClues Infotech is one of the best flutter app development company in USA & India. Our diverse and experienced team of developers can help you sketch the smartest and quickest solution for your mobile app development projects with the most superior technology.
For more info:
Website: https://www.appcluesinfotech.com/
Email: info@appcluesinfotech.com
Call: +1-978-309-9910
#top flutter app development company in usa #best flutter app development service #best flutter app development company #hire flutter app developers #flutter app development company in usa & india #custom flutter app development service