Flutter Dev

Flutter Dev

1552919734

Flutter Todos Tutorial with “flutter_bloc”

In the following tutorial, we’re going to build a Todos App in Flutter using the Bloc Library. By the time we’re done, our app should look something like this:

Let’s get started!

Setup

We’ll start off by creating a brand new Flutter project

flutter create flutter_todos

We can then replace the contents of pubspec.yaml with:

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.

Todos Repository

In this tutorial we’re not going to go into the implementation details of the TodosRepository because it was implemented by Brian Egan and is shared among all of the Todo Architecture Samples.

At a high level, the TodosRepository will expose a method to loadTodos and to saveTodos. That’s pretty much all we need to know so for the rest of the tutorial we’ll focus on the Bloc and Presentation layers.

Todos Bloc

Our TodosBloc will be responsible for converting TodosEvents into TodosStates 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.

States

Let’s create blocs/todos/todos_state.dart and define the different states we’ll need to handle.

The three states we will implement are:

  • TodosLoading - the state while our application is fetching todos from the repository.
  • TodosLoaded - the state of our application after the todos have successfully been loaded.
  • TodosNotLoaded - the state of our application if the todos were not successfully loaded.
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:

  • 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.

Bloc

Let’s create blocs/todos/todos_bloc.dart and get started! We just need to implement initialState and mapEventToState.

Tip*: Check out the* Bloc VSCode Extension which provides tools for effectively creating blocs for both Flutter and AngularDart apps.

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.

Barrel File

Now that we’re done with our TodosBloc we can create a barrel file to export all of our bloc files and make it convenient to import them later on.

Create blocs/todos/todos.dart and export the bloc, events, and states:

export './todos_bloc.dart';
export './todos_event.dart';
export './todos_state.dart';

Filtered Todos Bloc

Our TodosBloc will be responsible for converting TodosEvents into TodosStates 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 }

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:

  • 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.

Events

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!

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(
    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.

Barrel File

Just like before, we can create a barrel file to make it more convenient to import the various filtered todos classes.

Create blocs/filtered_todos/filtered_todos.dart and export the three files:

export './filtered_todos_bloc.dart';
export './filtered_todos_event.dart';
export './filtered_todos_state.dart';

Stats Bloc

Our TodosBloc will be responsible for converting TodosEvents into TodosStates 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.

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(
    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.

Tab Bloc

Our TodosBloc will be responsible for converting TodosEvents into TodosStates 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 }

Event

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 }';
}

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(
    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.

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';

Bloc Delegate

Before we move on to the presentation layer, we will implement our own BlocDelegate which will allow us to handle all state changes and errors in a single place. It’s really useful for things like developer logs or analytics.

Create blocs/simple_bloc_delegate.dart and let’s get started.

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…

Blocs Barrel File

Now that we have all of our blocs implemented we can create a barrel file. Create blocs/blocs.dart and export all of our blocs so that we can conveniently import any bloc code with a single import.

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.

Screens

Home Screen

Our TodosBloc will be responsible for converting TodosEvents into TodosStates and will manage the list of todos in our application.
Let’s create a new directory called screens where we will put all of our new screen widgets and then create screens/home_screen.dart.

Our HomeScreen will be a StatefulWidget because it will need to create and dispose the TabBloc, FilteredTodosBloc, and StatsBloc.

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.

Details Screen

Our TodosBloc will be responsible for converting TodosEvents into TodosStates and will manage the list of todos in our application.
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(
      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.

Add/Edit Screen

Our TodosBloc will be responsible for converting TodosEvents into TodosStates and will manage the list of todos in our application.
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:

  • 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.

Screens Barrel File

Create screens/screens.dart and export all three.

export './add_edit_screen.dart';
export './details_screen.dart';
export './home_screen.dart';

Widgets

FilterButton

Our TodosBloc will be responsible for converting TodosEvents into TodosStates and will manage the list of todos in our application.
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(
        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.

Extra Actions

Our TodosBloc will be responsible for converting TodosEvents into TodosStates and will manage the list of todos in our application.
Since this widget doesn’t care about the filters it will interact with the TodosBloc instead of the FilteredTodosBloc.

Let’s create widgets/extra_actions.dart and implement it.

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.

Tab Selector

Our TodosBloc will be responsible for converting TodosEvents into TodosStates and will manage the list of todos in our application.
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

Our TodosBloc will be responsible for converting TodosEvents into TodosStates and will manage the list of todos in our application.
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 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.

Todo Item

Our TodosBloc will be responsible for converting TodosEvents into TodosStates and will manage the list of todos in our application.
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

Our TodosBloc will be responsible for converting TodosEvents into TodosStates and will manage the list of todos in our application.
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

Our TodosBloc will be responsible for converting TodosEvents into TodosStates and will manage the list of todos in our application.
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

Our TodosBloc will be responsible for converting TodosEvents into TodosStates and will manage the list of todos in our application.
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) {
    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.

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(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

What is GEEK

Buddha Community

Flutter Todos Tutorial with “flutter_bloc”

Google's Flutter 1.20 stable announced with new features - Navoki

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:

Performance improvements for Flutter and Dart

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.

sksl_warm-up

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 for mobile text fields

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.

flutter_autofill

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.

Updated Material Slider, RangeSlider, TimePicker, and DatePicker

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 RangeSliderDatePicker with support for date range and time picker with the new style.

flutter_DatePicker

New pubspec.yaml format

Other 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.

Preview of embedded Dart DevTools in Visual Studio Code

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.

Network tracking

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.

Generate type-safe platform channels for platform interop

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 Dartmethod.

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

Terry  Tremblay

Terry Tremblay

1598396940

What is Flutter and why you should learn it?

Flutter is an open-source UI toolkit for mobile developers, so they can use it to build native-looking** Android and iOS** applications from the same code base for both platforms. Flutter is also working to make Flutter apps for Web, PWA (progressive Web-App) and Desktop platform (Windows,macOS,Linux).

flutter-mobile-desktop-web-embedded_min

Flutter was officially released in December 2018. Since then, it has gone a much stronger flutter community.

There has been much increase in flutter developers, flutter packages, youtube tutorials, blogs, flutter examples apps, official and private events, and more. Flutter is now on top software repos based and trending on GitHub.

Flutter meaning?

What is Flutter? this question comes to many new developer’s mind.

humming_bird_dart_flutter

Flutter means flying wings quickly, and lightly but obviously, this doesn’t apply in our SDK.

So Flutter was one of the companies that were acquired by **Google **for around $40 million. That company was based on providing gesture detection and recognition from a standard webcam. But later when the Flutter was going to release in alpha version for developer it’s name was Sky, but since Google already owned Flutter name, so they rename it to Flutter.

Where Flutter is used?

Flutter is used in many startup companies nowadays, and even some MNCs are also adopting Flutter as a mobile development framework. Many top famous companies are using their apps in Flutter. Some of them here are

Dream11

Dream11

NuBank

NuBank

Reflectly app

Reflectly app

Abbey Road Studios

Abbey Road Studios

and many more other apps. Mobile development companies also adopted Flutter as a service for their clients. Even I was one of them who developed flutter apps as a freelancer and later as an IT company for mobile apps.

Flutter as a service

#dart #flutter #uncategorized #flutter framework #flutter jobs #flutter language #flutter meaning #flutter meaning in hindi #google flutter #how does flutter work #what is flutter

Adobe XD plugin for Flutter with CodePen Tutorial

Recently Adobe XD releases a new version of the plugin that you can use to export designs directly into flutter widgets or screens. Yes, you read it right, now you can make and export your favorite design in Adobe XD and export all the design in the widget form or as a full-screen design, this can save you a lot of time required in designing.

What we will do?
I will make a simple design of a dialogue box with a card design with text over it as shown below. After you complete this exercise you can experiment with the UI. You can make your own components or import UI kits available with the Adobe XD.

#developers #flutter #adobe xd design export to flutter #adobe xd flutter code #adobe xd flutter code generator - plugin #adobe xd flutter plugin #adobe xd flutter plugin tutorial #adobe xd plugins #adobe xd to flutter #adobe xd tutorial #codepen for flutter.

Punith Raaj

1644991598

The Ultimate Guide To Tik Tok Clone App With Firebase - Ep 2

The Ultimate Guide To Tik Tok Clone App With Firebase - Ep 2
In this video, I'm going to show you how to make a Cool Tik Tok App a new Instagram using Flutter,firebase and visual studio code.

In this tutorial, you will learn how to Upload a Profile Pic to Firestore Data Storage.

🚀 Nice, clean and modern TikTok Clone #App #UI made in #Flutter⚠️

Starter Project : https://github.com/Punithraaj/Flutter_Tik_Tok_Clone_App/tree/Episode1

► Timestamps 
0:00 Intro 0:20 
Upload Profile Screen 
16:35 Image Picker
20:06 Image Cropper 
24:25 Firestore Data Storage Configuration.

⚠️ IMPORTANT: If you want to learn, I strongly advise you to watch the video at a slow speed and try to follow the code and understand what is done, without having to copy the code, and then download it from GitHub.

► Social Media 
GitHub: https://github.com/Punithraaj/Flutter_Tik_Tok_Clone_App.git
LinkedIn: https://www.linkedin.com/in/roaring-r...
Twitter: https://twitter.com/roaringraaj
Facebook: https://www.facebook.com/flutterdartacademy

► Previous Episode : https://youtu.be/QnL3fr-XpC4
► Playlist: https://youtube.com/playlist?list=PL6vcAuTKAaYe_9KQRsxTsFFSx78g1OluK

I hope you liked it, and don't forget to like,comment, subscribe, share this video with your friends, and star the repository on GitHub!
⭐️ Thanks for watching the video and for more updates don't forget to click on the notification. 
⭐️Please comment your suggestion for my improvement. 
⭐️Remember to like, subscribe, share this video, and star the repo on Github :)

Hope you enjoyed this video!
If you loved it, you can Buy me a coffee : https://www.buymeacoffee.com/roaringraaj

LIKE & SHARE & ACTIVATE THE BELL Thanks For Watching :-)
 
https://youtu.be/F_GgZVD4sDk

#flutter tutorial - tiktok clone with firebase #flutter challenge @tiktokclone #fluttertutorial firebase #flutter firebase #flutter pageview #morioh #flutter

Punith Raaj

1640672627

Flutter Hotel Booking UI - Book your Stay At A New Hotel With Flutter - Ep1

https://youtu.be/-tHUmjIkGJ4
Flutter Hotel Booking UI - Book your Stay At A New Hotel With Flutter - Ep1
#flutter #fluttertravelapp #hotelbookingui #flutter ui design 

In this video, I'm going to show you how to make a Cool Hotel Booking App using Flutter and visual studio code. 

In this tutorial, you will learn how to create a Splash Screen and Introduction Screen, how to implement a SmoothPageIndicator in Flutter. 

🚀 Nice, clean and modern Hotel Booking #App #UI made in #Flutter 

⚠️ IMPORTANT: If you want to learn, I strongly advise you to watch the video at a slow speed and try to follow the code and understand what is done, without having to copy the code, and then download it from GitHub. 

► Social Media 

    GitHub: https://github.com/Punithraaj 

    LinkedIn: https://www.linkedin.com/in/roaring-r...

    Twitter: https://twitter.com/roaringraaj

    Facebook: https://www.facebook.com/flutterdartacademy

I hope you liked it, and don't forget to like,comment, subscribe, share this video with your friends, and star the repository on GitHub! 

⭐️ Thanks for watching the video and for more updates don't forget to click on the notification.⭐️Please comment your suggestion for my improvement. ⭐️Remember to like, subscribe, share this video, and star the repo on Github :)Hope you enjoyed this video! If you loved it, you can Buy me a coffee : https://www.buymeacoffee.com/roaringraaj

#flutter riverpod #flutter travel app #appointment app flutter #morioh