Build Firestore Todo App in flutter using “flutter_bloc”

Build Firestore Todo App in flutter using “flutter_bloc”

In this tutorial, we’re going to build a reactive Todos App which hooks up to Firestore in flutter using the bloc and flutter_bloc packages

We’re going to be building on top of the flutter todos example so we won’t go into the UI since it will all be the same. Instead, we’re going to focus on the repository layer as well as some of the blocs.

Our finished product will look very similar to the previous todos example, however, our todos will be synced across all devices and will update in real-time.

Before we get started, I want to give a special thanks to warriorCoder for helping make this tutorial possible!

Repositories

We’ll start off in the repository layer with the TodosRepository.

Todos Repository

Create a new package at the root level of our app called todos_repository.

Note: The reason for making the repository a standalone package is to illustrate that the repository should be decoupled from the application and can be reused across multiple apps.

Inside our todos_repository we need to create the following folder/file structure.

├── lib

│   ├── src

│   │   ├── entities

│   │   │   ├── entities.dart

│   │   │   └── todo_entity.dart

│   │   ├── models

│   │   │   ├── models.dart

│   │   │   └── todo.dart

│   │   ├── todo.dart

│   │   ├── todo_entity.dart

│   │   ├── todos_repository.dart

│   │   └── firebase_todos_repository.dart

│   └── todos_repository.dart

└── pubspec.yaml

Dependencies

The pubspec.yaml should look like:

name: todos_repository

version: 1.0.0+1

environment: sdk: ">=2.1.0 <3.0.0"

dependencies: flutter: sdk: flutter cloud_firestore: ^0.12.7 rxdart: ^0.22.0 equatable: ^0.3.0 firebase_core: ^0.4.0+7

Note: We can immediately see our todos_repository has a dependency on firebase_core and cloud_firestore.

Package Root

The todos_repository.dart directly inside lib is responsible for the public exports in our package and should look like:

library todos_repository;

export 'src/firebase_todos_repository.dart'; export 'src/models/models.dart'; export 'src/todos_repository.dart';

Entities

Entities represent the data provided by our data provider.

The entities.dart file is a barrel file that exports the single todo_entity.dart file.

export 'todo_entity.dart';

Our TodoEntity is the representation of our Todo inside Firestore.

Let’s create todo_entity.dart and implement it.

import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:equatable/equatable.dart';

class TodoEntity extends Equatable { final bool complete; final String id; final String note; final String task;

TodoEntity(this.task, this.id, this.note, this.complete);

Map<String, Object> toJson() { return { 'complete': complete, 'task': task, 'note': note, 'id': id, }; }

@override String toString() { return 'TodoEntity { complete: $complete, task: $task, note: $note, id: $id }'; }

static TodoEntity fromJson(Map<String, Object> json) { return TodoEntity( json['task'] as String, json['id'] as String, json['note'] as String, json['complete'] as bool, ); }

static TodoEntity fromSnapshot(DocumentSnapshot snap) { return TodoEntity( snap.data['task'], snap.documentID, snap.data['note'], snap.data['complete'], ); }

Map<String, Object> toDocument() { return { 'complete': complete, 'task': task, 'note': note, }; } }

The toJson and fromJson are standard methods for converting to/from json. The fromSnapshot and toDocument are specific to Firestore.

Note: Firestore will automatically create the id for the document when we insert it. As such we don’t want to duplicate data by storing the id in an id field.

Models

Models will contain plain dart classes which we will work with in our Flutter Application. Having the separation between models and entities allows us to switch our data provider at any time and only have to change the the toEntity and fromEntity conversion in our model layer.

Our models.dart is another barrel file which exports our single todo model.

export 'todo.dart';

Next, we need to create our todo.dart model.

import 'package:meta/meta.dart';
import '../entities/entities.dart';

@immutable class Todo { 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;

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 int get hashCode => complete.hashCode ^ task.hashCode ^ note.hashCode ^ id.hashCode;

@override bool operator ==(Object other) => identical(this, other) || other is Todo && runtimeType == other.runtimeType && complete == other.complete && task == other.task && note == other.note && id == other.id;

@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, ); } }

It’s important to note that having a model gives us the flexibility to change providers. Our application will always interact with our model (not our entity) so if we change data providers all we need to do is update our toEntity and fromEntity methods without needing to refactor our entire application.

Todos Repository

TodosRepository is our abstract base class which we can extend whenever we want to integrate with a different TodosProvider.

Let’s create todos_repository.dart

import 'dart:async';

import 'package:todos_repository/todos_repository.dart';

abstract class TodosRepository { Future<void> addNewTodo(Todo todo);

Future<void> deleteTodo(Todo todo);

Stream<List<Todo>> todos();

Future<void> updateTodo(Todo todo); }

Note: Because we have this interface it is easy to add another type of datastore. If, for example, we wanted to use something like sembast all we would need to do is create a separate repository for handling the sembast specific code.

Firebase Todos Repository

FirebaseTodosRepository manages the integration with Firestore and implements our TodosRepositoryinterface.

Let’s create firebase_todos_repository.dart and implement it!

import 'dart:async';

import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:todos_repository/todos_repository.dart'; import 'entities/entities.dart';

class FirebaseTodosRepository implements TodosRepository { final todoCollection = Firestore.instance.collection('todos');

@override Future<void> addNewTodo(Todo todo) { return todoCollection.add(todo.toEntity().toDocument()); }

@override Future<void> deleteTodo(Todo todo) async { return todoCollection.document(todo.id).delete(); }

@override Stream<List<Todo>> todos() { return todoCollection.snapshots().map((snapshot) { return snapshot.documents .map((doc) => Todo.fromEntity(TodoEntity.fromSnapshot(doc))) .toList(); }); }

@override Future<void> updateTodo(Todo update) { return todoCollection .document(update.id) .updateData(update.toEntity().toDocument()); } }

That’s it for our TodosRepository, next we need to create a simple UserRepository to manage authenticating our users.

User Repository

Create a new package at the root level of our app called useer_repository.

Inside our user_repository create the following folder/file structure.

├── lib

│   ├── src

│   │   └── user_repository.dart

│   └── user_repository.dart

└── pubspec.yaml

Dependencies

The pubspec.yaml should look like:

name: user_repository

version: 1.0.0+1

environment: sdk: ">=2.1.0 <3.0.0"

dependencies: flutter: sdk: flutter firebase_auth: ^0.11.1+12

Note: We can immediately see our user_repository has a dependency on firebase_auth.

Package Root

The user_repository.dart directly inside lib should look like:

library user_repository;

export 'src/user_repository.dart';

User Repository

UserRepository is our abstract base class which we can extend whenever we want to integrate with a different provider`.

Let’s create user_repository.dart and implement it!

abstract class UserRepository {
  Future<bool> isAuthenticated();

Future<void> authenticate();

Future<String> getUserId(); }

Firebase User Repository

FirebaseUserRepository manages the integration with Firebase and implements our UserRepositoryinterface.

Let’s open firebaseuserrepository.dart and implement it!

import 'package:firebase_auth/firebase_auth.dart';
import 'package:user_repository/user_repository.dart';

class FirebaseUserRepository implements UserRepository { final FirebaseAuth _firebaseAuth;

FirebaseUserRepository({FirebaseAuth firebaseAuth}) : _firebaseAuth = firebaseAuth ?? FirebaseAuth.instance;

Future<bool> isAuthenticated() async { final currentUser = await _firebaseAuth.currentUser(); return currentUser != null; }

Future<void> authenticate() { return _firebaseAuth.signInAnonymously(); }

Future<String> getUserId() async { return (await _firebaseAuth.currentUser()).uid; } }

That’s it for our UserRepository, next we need to setup our Flutter app to use our new repositories.

Flutter App

Setup

Let’s create a new Flutter app called flutter_firestore_todos. We can replace the contents of the pubspec.yaml with the following:

name: flutter_firestore_todos
description: A new Flutter project.

version: 1.0.0+1

environment: sdk: ">=2.1.0 <3.0.0"

dependencies: flutter: sdk: flutter flutter_bloc: ^0.20.0 todos_repository: path: todos_repository user_repository: path: user_repository equatable: ^0.3.0

flutter: uses-material-design: true

Note: We’re adding our todos_repository and user_repository as external dependencies.

Authentication Bloc

Since we want to be able to sign in our users, we’ll need to create an AuthenticationBloc.

Note: If you haven’t already checked out the flutter firebase login tutorial, I highly recommend checking it out now because we’re simply going to reuse the same AuthenticationBloc.

Authentication Events

import 'package:equatable/equatable.dart';
import 'package:meta/meta.dart';

@immutable abstract class AuthenticationEvent extends Equatable { AuthenticationEvent([List props = const []]) : super(props); }

class AppStarted extends AuthenticationEvent { @override String toString() => 'AppStarted'; }

Authentication States

import 'package:equatable/equatable.dart';
import 'package:meta/meta.dart';

@immutable abstract class AuthenticationState extends Equatable { AuthenticationState([List props = const []]) : super(props); }

class Uninitialized extends AuthenticationState { @override String toString() => 'Uninitialized'; }

class Authenticated extends AuthenticationState { final String userId;

Authenticated(this.userId) : super([userId]);

@override String toString() => 'Authenticated { userId: $userId }'; }

class Unauthenticated extends AuthenticationState { @override String toString() => 'Unauthenticated'; }

Authentication Bloc

import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:meta/meta.dart';
import 'package:user_repository/user_repository.dart';
import 'package:flutter_firestore_todos/blocs/authentication_bloc/bloc.dart';

class AuthenticationBloc extends Bloc<AuthenticationEvent, AuthenticationState> { final UserRepository _userRepository;

AuthenticationBloc({@required UserRepository userRepository}) : assert(userRepository != null), _userRepository = userRepository;

@override AuthenticationState get initialState => Uninitialized();

@override Stream<AuthenticationState> mapEventToState( AuthenticationEvent event, ) async* { if (event is AppStarted) { yield* _mapAppStartedToState(); } }

Stream<AuthenticationState> mapAppStartedToState() async* { try { final isSignedIn = await _userRepository.isAuthenticated(); if (!isSignedIn) { await _userRepository.authenticate(); } final userId = await _userRepository.getUserId(); yield Authenticated(userId); } catch () { yield Unauthenticated(); } } }

Now that our AuthenticationBloc is finished, we need to modify the TodosBloc from the original todos tutorial to consume the new TodosRepository.

Todos Bloc

import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:meta/meta.dart';
import 'package:flutter_firestore_todos/blocs/todos/todos.dart';
import 'package:todos_repository/todos_repository.dart';

class TodosBloc extends Bloc<TodosEvent, TodosState> { final TodosRepository _todosRepository; StreamSubscription _todosSubscription;

TodosBloc({@required TodosRepository todosRepository}) : assert(todosRepository != null), _todosRepository = 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(); } else if (event is TodosUpdated) { yield* _mapTodosUpdateToState(event); } }

Stream<TodosState> _mapLoadTodosToState() async* { _todosSubscription?.cancel(); _todosSubscription = _todosRepository.todos().listen( (todos) { dispatch( TodosUpdated(todos), ); }, ); }

Stream<TodosState> _mapAddTodoToState(AddTodo event) async* { _todosRepository.addNewTodo(event.todo); }

Stream<TodosState> _mapUpdateTodoToState(UpdateTodo event) async* { _todosRepository.updateTodo(event.updatedTodo); }

Stream<TodosState> _mapDeleteTodoToState(DeleteTodo event) async* { _todosRepository.deleteTodo(event.todo); }

Stream<TodosState> _mapToggleAllToState() async* { final state = currentState; if (state is TodosLoaded) { final allComplete = state.todos.every((todo) => todo.complete); final List<Todo> updatedTodos = state.todos .map((todo) => todo.copyWith(complete: !allComplete)) .toList(); updatedTodos.forEach((updatedTodo) { _todosRepository.updateTodo(updatedTodo); }); } }

Stream<TodosState> _mapClearCompletedToState() async* { final state = currentState; if (state is TodosLoaded) { final List<Todo> completedTodos = state.todos.where((todo) => todo.complete).toList(); completedTodos.forEach((completedTodo) { _todosRepository.deleteTodo(completedTodo); }); } }

Stream<TodosState> _mapTodosUpdateToState(TodosUpdated event) async* { yield TodosLoaded(event.todos); }

@override void dispose() { _todosSubscription?.cancel(); super.dispose(); } }

The main difference between our new TodosBloc and the original one is in the new one, everything is Stream based rather than Future based.

Stream<TodosState> _mapLoadTodosToState() async* {
  _todosSubscription?.cancel();
  _todosSubscription = _todosRepository.todos().listen(
    (todos) {
      dispatch(
        TodosUpdated(todos),
      );
    },
  );
}

When we load our todos, we are subscribing to the TodosRepository and every time a new todo comes in, we dispatch a TodosUpdated event. We then handle all TodosUpdates via:

Stream<TodosState> _mapTodosUpdateToState(TodosUpdated event) async* {
  yield TodosLoaded(event.todos);
}

Putting it all together

The last thing we need to modify is our main.dart.

import 'package:flutter/material.dart';
import 'package:bloc/bloc.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_firestore_todos/blocs/authentication_bloc/bloc.dart';
import 'package:todos_repository/todos_repository.dart';
import 'package:flutter_firestore_todos/blocs/blocs.dart';
import 'package:flutter_firestore_todos/screens/screens.dart';
import 'package:user_repository/user_repository.dart';

void main() { BlocSupervisor.delegate = SimpleBlocDelegate(); runApp(TodosApp()); }

class TodosApp extends StatelessWidget { @override Widget build(BuildContext context) { return MultiBlocProvider( providers: [ BlocProvider<AuthenticationBloc>( builder: (context) { return AuthenticationBloc( userRepository: FirebaseUserRepository(), )..dispatch(AppStarted()); }, ), BlocProvider<TodosBloc>( builder: (context) { return TodosBloc( todosRepository: FirebaseTodosRepository(), )..dispatch(LoadTodos()); }, ) ], child: MaterialApp( title: 'Firestore Todos', routes: { '/': (context) { return BlocBuilder<AuthenticationBloc, AuthenticationState>( builder: (context, state) { if (state is Authenticated) { final todosBloc = BlocProvider.of<TodosBloc>(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(), ); } if (state is Unauthenticated) { return Center( child: Text('Could not authenticate with Firestore'), ); } return Center(child: CircularProgressIndicator()); }, ); }, '/addTodo': (context) { final todosBloc = BlocProvider.of<TodosBloc>(context); return AddEditScreen( onSave: (task, note) { todosBloc.dispatch( AddTodo(Todo(task, note: note)), ); }, isEditing: false, ); }, }, ), ); } }

The main difference is we’ve wrapped our entire application in a MultiBlocProvider which initializes and provides the AuthenticationBloc and TodosBloc. We then, only render the todos app if the AuthenticationState is Authenticated using BlocBuilder. Everything else remains the same as in the previous todos tutorial.

That’s all there is to it! We’ve now successfully implemented a firestore todos app in flutter using the bloc and flutter_bloc packages and we’ve successfully separated our presentation layer from our business logic while also building an app that updates in real-time.

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

Further reading

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 - Advanced Course

Flutter Tutorial - Flight List UI Example In Flutter

Flutter Tutorial for Beginners - Full Tutorial

Using Go Library in Flutter

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 ios

Bootstrap 5 Complete Course with Examples

Bootstrap 5 Tutorial - Bootstrap 5 Crash Course for Beginners

Nest.JS Tutorial for Beginners

Hello Vue 3: A First Look at Vue 3 and the Composition API

Building a simple Applications with Vue 3

Deno Crash Course: Explore Deno and Create a full REST API with Deno

How to Build a Real-time Chat App with Deno and WebSockets

Convert HTML to Markdown Online

HTML entity encoder decoder Online

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

Google has announced new flutter 1.20 stable with many improvements, and features, enabling flutter for Desktop and Web

How To Succeed In Mobile App Wireframe Design?

This article covers everything about mobile app wireframe design: what to do and what not, tools used in designing a mobile or web app wireframe, and more.

How long does it take to develop/build an app?

This article covers A-Z about the mobile and web app development process and answers your question on how long does it take to develop/build an app.

Top 25 Flutter Mobile App Templates in 2020

Flutter has been booming worldwide from the past few years. While there are many popular mobile app development technologies out there, Flutter has managed to leave its mark in the mobile application development world. In this article, we’ve curated the best Flutter app templates available on the market as of July 2020.

Best Mobile App Development Company | Android and iOS Apps

iPrism Tech is a one of the best and offshore mobile app development company in India, Saudi Arabia and USA. We are a major providers of android, iphone and ipad mobile app development services at economical prices.