Codementor Events

When Firebase meets BLoC Pattern

Published Jan 21, 2019

Hi Flutterian! I am back with another brand new article. This time I am excited to show you all how I integrated some of the Firebase plugins with the famous BLoC pattern in a Flutter project. Before I start my show. I would like to wish you all a very Happy new year 2k19 😃 🎊.

Goal

As I see a lot of posts regarding integration of Firebase plugins in Flutter projects but what I don’t see is how to manage these plugins using an architecture like BLoC in a Flutter project. So that the project can be scalable and testable in the easiest way possible. My main goal behind writing this article is to show you all how to implement different Firebase plugins’ operations using BLoC pattern.To be more specific regarding the Firebase plugins, I will be using Firestore and Firebase_ML_Vision in this post. Enough talking, let’s get straight to business.

Prerequisite

To take full advantage of this article I expect that you have built a simple Flutter app using the BLoC pattern. If it’s a yes than it will be a cake walk for you when going through the content. If its a no then head over to these two amazing articles PART1 and PART2 on BLoC pattern.

What are we building?

As it’s the beginning of a new year everyone have some milestones or goals that they want to achieve this year. Some of them can be like “I won’t eat cheese burst pizza” 😜 or “I will travel the whole world” 🗺 etc. It’s good to have some goals or milestones that you want to achieve. It keeps you motivated and focused. But in today’s busy world you might forget your goals. Keeping this in mind I thought to build an app where you can store your goals and share it with the whole world. If you are not able to find a great idea to set it as your goal you can see someone else goal and make it yours as well 😄. Interesting ? I know it’s a yes. So without further delay let’s start building.

App Flow

This is the basic flow of the app we will be building. As you can see the app has some basic features. I want to keep things simple because the main aim of this article is not to teach you how to build an app with Firestore but how to manage all your firebase plugins operations in the BLoC pattern.


Basic flow of the app

Features of the app

These are the features we will be building:

  1. Registration/Login
  2. Add Goals
  3. View yours and other users goals
  4. Delete your goals

By now you know what are the features we need to build. The first step should be to design the Firestore database structure in which the user’s data will be stored. Designing the database structure first will help us implement all the CRUD operations in our app. Before you go to the next section I expect you to have a basic understanding of the data model(collections, document etc) which Cloud Firestore follows to store data.

Github link to the project.

Database design

The following screenshot of my Firestore database will give most of you a clear understanding of how I am going to store the user’s data:


Firestore database

I have created a collection and named it as users which will hold a list of documents. Each document represents a user. Inside each document I will be storing the user’s details like email, password and the list of goals added by him/her.

Data type of each field in a document:

  1. email is of type String
  2. password is of type String
  3. goalAdded is of type Boolean
  4. goals is of type Map

Note : Here goalAdded is used as a flag in the search query to find out all the users who have added goals. This will a lot faster to get all the goals from different users.

In the next section I will be giving you a small recap about BLoC pattern which will help us to visualise how the architecture of the app would be.

About BLoC

Just a recap on what BLoC pattern is made of:

Building blocks of BLoC pattern:

  1. Network Provider/Data Layer : This part will be holding our Firebase plugins(Firestore and ML Vision) operations. For example: logic for storing of notes to the cloud will be written here.
  2. Repository : The Repository pattern is used to decouple the business logic and the data access layers in our application. It will expose all the Data layer operations to the BLoC layer.
  3. BLoC : This layer will be handling our business logics. It will act as a controller between UI and the data layers. For example: email and password validation logic will be written here.
  4. UI Screen : The human interaction area. This is the place where the user will be adding the goals.

Let’s Code

Now let’s start building our app. But wait, there is a big question many developers have by this time and I would like to answer it now. Which layer should I start with?

Pro answer : Always go from bottom to top. I mean start with the Providers/Data LayerRepositoryBLoC → finally your UI. This will help you test all your algorithms and logics at an early stage.

Below are the steps I followed to get this project complete in two days:

  1. Create a new Flutter project and setup Firebase in your project by following this codelab. It will hardly take 10 mins to setup.
  2. Once you are done with the Firebase setup. Add the following dependencies to your pubspec.yaml :
dependencies: flutter: sdk: flutter
  cloud_firestore: ^0.8.2+3
  firebase_ml_vision: ^0.2.0+2
  image_picker: ^0.4.10
  rxdart: ^0.20.0

cloud_firestore for storing the user details and goals to the cloud. I also added ml_vision and image_picker so as to make addition of goal a little exciting. If users have written down their goals in their laptop than they can just scan their goals through the app and save it. rxdart for making the UI listen to changes in the data layer reactively.

  1. Clear all the code from the main.dart file and add below code:
import 'package:flutter/material.dart';
import 'src/app.dart';

void main() => runApp(MyApp());
  1. Under the lib directory create a src package. Under it create a dart file and name it as app.dart. Copy and paste below code into it:
import 'package:flutter/material.dart';
import 'ui/login.dart';
import 'blocs/goals_bloc_provider.dart';
import 'blocs/login_bloc_provider.dart';

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) { return LoginBlocProvider(
      child: GoalsBlocProvider(
        child: MaterialApp(
          theme: ThemeData(
            accentColor: Colors.black,
            primaryColor: Colors.amber,
          ),
          home: Scaffold(
            appBar: AppBar(
              title: Text(
                "Goals",
                style: TextStyle(color: Colors.black),
              ),
              backgroundColor: Colors.amber,
              elevation: 0.0,
            ),
            body: LoginScreen(),
          ),
        ),
      ),
    );
  }
}

You must be seeing a lot of errors. Don’t panic I will be resolving them in the next few sections. Take away from the above code is I am using two InheritedWidgets which are my BLoC providers. These providers will help me get access to the BLoCs(Login BLoC and Goals BLoC) object throughout my widget tree.

  1. As I told above we should start implementing the lower layer first i.e the Providers/Data layer. Before creating a new package for the data layer pause for a while and try to find out what are the different type of data sources from where we will be getting or storing data.

Types of resources:

a) ML vision : I will be using ml_vision plugin to extract text(goals) from images taken by users, which makes ml_vision a candidate to be a resource provider.

b) Cloud Firestore : Another resource will be our Cloud Firestore which will store goals added by users and provide all the saved goals from cloud when required.

Now we found out what are the different type of sources. So let’s code them up.

  1. Create a resources package under the lib directory. Under the resources package create 2 files and name them as firestore_provider.dart and mlkit_provider.dart.

Copy and paste below code in firestore_provider.dart file:

import 'package:cloud_firestore/cloud_firestore.dart';

class FirestoreProvider {
  Firestore _firestore = Firestore.instance;

  Future<int> authenticateUser(String email, String password) async {
    final QuerySnapshot result = await _firestore .collection("users")
        .where("email", isEqualTo: email)
        .getDocuments();
    final List<DocumentSnapshot> docs = result.documents;
    if (docs.length == 0) {
      return 0;
    } else {
      return 1;
    }
  }

  Future<void> registerUser(String email, String password) async {
    return _firestore .collection("users")
        .document(email)
        .setData({'email': email, 'password': password, 'goalAdded': false});
  }

  Future<void> uploadGoal(String title, String documentId, String goal) async {
    DocumentSnapshot doc =
        await _firestore.collection("users").document(documentId).get();
    Map<String, String> goals = doc.data["goals"] != null ? doc.data["goals"].cast<String, String>()
        : null;
    if (goals != null) {
      goals[title] = goal;
    } else {
      goals = Map();
      goals[title] = goal;
    }
    return _firestore .collection("users")
        .document(documentId)
        .setData({'goals': goals, 'goalAdded': true}, merge: true);
  }

  Stream<DocumentSnapshot> myGoalList(String documentId) {
    return _firestore.collection("users").document(documentId).snapshots();
  }

  Stream<QuerySnapshot> othersGoalList() {
    return _firestore .collection("users")
        .where('goalAdded', isEqualTo: true)
        .snapshots();
  }

  void removeGoal(String title, String documentId) async {
    DocumentSnapshot doc =
        await _firestore.collection("users").document(documentId).get();
    Map<String, String> goals = doc.data["goals"].cast<String, String>();
    goals.remove(title);
    if (goals.isNotEmpty) {
      _firestore .collection("users")
          .document(documentId)
          .updateData({"goals": goals});
    } else {
      _firestore .collection("users")
          .document(documentId)
          .updateData({'goals': FieldValue.delete(), 'goalAdded': false});
    }
  }
}

In the above code you will find different Cloud Firestore operations like creating users(registerUser), adding goals to the cloud(uploadGoal) etc.

Copy and paste below code in your mlkit_provider.dart file:

import 'dart:io';
import 'package:firebase_ml_vision/firebase_ml_vision.dart';

class MLkitProvider {
  FirebaseVision _firebaseVision = FirebaseVision.instance;

  Future<String> getImage(var image) async {
    File _image = image;
    final FirebaseVisionImage _visionImage = FirebaseVisionImage.fromFile(
        _image);
    final TextRecognizer _textRecognizer = _firebaseVision.textRecognizer();
    final VisionText _visionText = await _textRecognizer.processImage(_visionImage);
    String _detectedText = _visionText.text;
    return _detectedText;
  }

}

In the above code it’s just the MLKit text extraction logic. It will take an image as input and return the extracted text back to the user.

  1. As our data layer(resources) is set. It’s time to create the Repository class since it will be the bridge between the BLoC layer and the Data layer. Create a new file under the resources package and name it as repository.dart. Copy and paste below code in it:
import 'package:cloud_firestore/cloud_firestore.dart';

import 'firestore_provider.dart';
import 'mlkit_provider.dart';

class Repository {
  final _firestoreProvider = FirestoreProvider();
  final _mlkitProvider = MLkitProvider();

  Future<int> authenticateUser(String email, String password) =>
      _firestoreProvider.authenticateUser(email, password);

  Future<void> registerUser(String email, String password) =>
      _firestoreProvider.registerUser(email, password);

  Future<String> extractText(var image) => _mlkitProvider.getImage(image);

  Future<void> uploadGoal(String email, String title, String goal) =>
      _firestoreProvider.uploadGoal(title, email, goal);

  Stream<DocumentSnapshot> myGoalList(String email) =>
      _firestoreProvider.myGoalList(email);

  Stream<QuerySnapshot> othersGoalList() => _firestoreProvider.othersGoalList();

  void removeGoal(String title, email) =>
      _firestoreProvider.removeGoal(title, email);
}

As you can see all the methods from different Providers are exposed here. These methods will be consumed by different BLoCs(Login BLoC and Goal BLoC) in our app.

We are done implementing the Data Layer of our app. Now it’s time to move to the next layer i.e the BLoC layer.

  1. Create a blocs package under the src package. This package will hold all your BLoC files and its Providers. Here Providers are InheritedWidgets which will hold the BLoC objects and provide it to their children widgets.

Login BLoC

The first screen the user will be seeing is the Login/Registration screen.This screen have a BLoC associated with it. So let’s build the first BLoC for the Login/Registration screen. Create two files under the blocs package and name them as login_bloc.dart and login_bloc_provider.dart.

Copy and paste below code in login_bloc_provider.dart file:

import 'package:flutter/material.dart';
import 'login_bloc.dart';
export 'login_bloc.dart';
class LoginBlocProvider extends InheritedWidget{
  final bloc = LoginBloc();

  LoginBlocProvider({Key key, Widget child}) : super(key: key, child: child);

  bool updateShouldNotify(_) => true;

  static LoginBloc of(BuildContext context) {
    return (context.inheritFromWidgetOfExactType(LoginBlocProvider) as LoginBlocProvider).bloc;
  }
}

This LoginBlocProvider will hold the LoginBloc object and provide it to all its children widgets i.e the LoginScreen widget.

Copy and paste below code in login_bloc.dart file:

import 'dart:async';
import '../utils/strings.dart';

import '../resources/repository.dart';
import 'package:rxdart/rxdart.dart';

class LoginBloc {
  final _repository = Repository();
  final _email = BehaviorSubject<String>();
  final _password = BehaviorSubject<String>();
  final _isSignedIn = BehaviorSubject<bool>();

  Observable<String> get email => _email.stream.transform(_validateEmail);

  Observable<String> get password =>
      _password.stream.transform(_validatePassword);

  Observable<bool> get signInStatus => _isSignedIn.stream;

  String get emailAddress => _email.value;

  // Change data Function(String) get changeEmail => _email.sink.add;

  Function(String) get changePassword => _password.sink.add;

  Function(bool) get showProgressBar => _isSignedIn.sink.add;

  final _validateEmail =
      StreamTransformer<String, String>.fromHandlers(handleData: (email, sink) {
    if (email.contains('@')) {
      sink.add(email);
    } else {
      sink.addError(StringConstant.emailValidateMessage);
    }
  });

  final _validatePassword = StreamTransformer<String, String>.fromHandlers(
      handleData: (password, sink) {
    if (password.length > 3) {
      sink.add(password);
    } else {
      sink.addError(StringConstant.passwordValidateMessage);
    }
  });

  Future<int> submit() {
    return _repository.authenticateUser(_email.value, _password.value);
  }

  Future<void> registerUser() {
    return _repository.registerUser(_email.value, _password.value);
  }

  void dispose() async {
    await _email.drain();
    _email.close();
    await _password.drain();
    _password.close();
    await _isSignedIn.drain();
    _isSignedIn.close();
  }

  bool validateFields() {
    if (_email.value != null &&
        _email.value.isNotEmpty &&
        _password.value != null &&
        _password.value.isNotEmpty &&
        _email.value.contains('@') &&
        _password.value.length > 3) {
      return true;
    } else {
      return false;
    }
  }
}

If you see the above code you will find all the logic for checking if the email entered is valid or not(_validateEmail) and same goes for the password value(_validatePassword). There is a submit method which will register the user to the Cloud Firestore database. Here LoginBloc will hold all the business logics for the Login screen.

Goals BLoC

  1. As we are done with the login bloc it’s time to create the next bloc which will be used both by the Add goals and Show goals screen. Create two files goals_bloc.dart and goals_bloc_provider.dart under the bloc package.

Copy and paste below code in goals_bloc_provider.dart :

import 'package:flutter/material.dart';
import 'goals_bloc.dart';
export 'goals_bloc.dart';
class GoalsBlocProvider extends InheritedWidget{
  final bloc = GoalsBloc();

  GoalsBlocProvider({Key key, Widget child}) : super(key: key, child: child);

  bool updateShouldNotify(_) => true;

  static GoalsBloc of(BuildContext context) {
    return (context.inheritFromWidgetOfExactType(GoalsBlocProvider) as GoalsBlocProvider).bloc;
  }
}

Same as the login_bloc_provider.dart only change is, this Provider will provide the GoalsBloc object to its children widgets i.e the Add goals and Show goals screen.

Copy and paste below code in the goals_bloc.dart file:

import 'dart:async';
import 'package:cloud_firestore/cloud_firestore.dart';
import '../models/goal.dart';
import '../models/other_goal.dart';
import '../utils/strings.dart';

import '../resources/repository.dart';
import 'package:rxdart/rxdart.dart';

class GoalsBloc {
  final _repository = Repository();
  final _title = BehaviorSubject<String>();
  final _goalMessage = BehaviorSubject<String>();
  final _showProgress = BehaviorSubject<bool>();

  Observable<String> get name => _title.stream.transform(_validateName);

  Observable<String> get goalMessage =>
      _goalMessage.stream.transform(_validateMessage);

  Observable<bool> get showProgress => _showProgress.stream;

  Function(String) get changeName => _title.sink.add;

  Function(String) get changeGoalMessage => _goalMessage.sink.add;

  final _validateMessage = StreamTransformer<String, String>.fromHandlers(
      handleData: (goalMessage, sink) {
    if (goalMessage.length > 10) {
      sink.add(goalMessage);
    } else {
      sink.addError(StringConstant.goalValidateMessage);
    }
  });

  final _validateName = StreamTransformer<String, String>.fromHandlers(
      handleData: (String name, sink) {
    if (RegExp(r'[!@#<>?":_`~;[\]\\|=+)(*&^%0-9-]').hasMatch(name)) {
      sink.addError(StringConstant.nameValidateMessage);
    } else {
      sink.add(name);
    }
  });

  void submit(String email) {
    _showProgress.sink.add(true);
    _repository .uploadGoal(email, _title.value, _goalMessage.value)
        .then((value) {
      _showProgress.sink.add(false);
    });
  }

  void extractText(var image) {
    _repository.extractText(image).then((text) {
      _goalMessage.sink.add(text);
    });
  }

  Stream<DocumentSnapshot> myGoalsList(String email) {
    return _repository.myGoalList(email);
  }

  Stream<QuerySnapshot> othersGoalList() {
    return _repository.othersGoalList();
  }

  //dispose all open sink void dispose() async {
    await _goalMessage.drain();
    _goalMessage.close();
    await _title.drain();
    _title.close();
    await _showProgress.drain();
    _showProgress.close();
  }

  //Convert map to goal list List mapToList({DocumentSnapshot doc, List<DocumentSnapshot> docList}) {
    if (docList != null) {
      List<OtherGoal> goalList = [];
      docList.forEach((document) {
        String email = document.data[StringConstant.emailField];
        Map<String, String> goals =
            document.data[StringConstant.goalField] != null ? document.data[StringConstant.goalField].cast<String, String>()
                : null;
        if (goals != null) {
          goals.forEach((title, message) {
            OtherGoal otherGoal = OtherGoal(email, title, message);
            goalList.add(otherGoal);
          });
        }
      });
      return goalList;
    } else {
      Map<String, String> goals = doc.data[StringConstant.goalField] != null ? doc.data[StringConstant.goalField].cast<String, String>()
          : null;
      List<Goal> goalList = [];
      if (goals != null) {
        goals.forEach((title, message) {
          Goal goal = Goal(title, message);
          goalList.add(goal);
        });
      }
      return goalList;
    }
  }

  //Remove item from the goal list void removeGoal(String title, String email) {
    return _repository.removeGoal(title, email);
  }
}

As you can see above there are different methods for different operations like adding a goal to the cloud(submit), remove a goal (removeGoal) etc. All these methods are used by the Add and Show goals screen.

  1. In the goals_bloc.dart file I am using two Model classes to store the goals. Let’s quickly code them up. Create a models package under the lib package. Now create two files under the models package and name them as goal.dart and other_goal.dart.

Copy and paste below code in goal.dart file:

class Goal {
  final String _title;
  final String _message;
  int _id;

  Goal(this._title, this._message){
    this._id = DateTime.now().millisecondsSinceEpoch;
  }

  String get title => _title;

  String get message => _message;

  int get id => _id;
}

Code for other_goal.dart file:

class OtherGoal{
  final String _email;
  final String _title;
  final String _message;

  OtherGoal(this._email,this._title,this._message);

  String get email => _email;
  String get title => _title;
  String get message => _message;
}

At this point of time we are done with all the business logic implementation. Now we are only left with the UI layer. This is the easiest layer out of all the layers we have implemented so far. Let’s code them quickly and wrap it up.

Reminder : Always start working from the lower layer first and build the top layer i.e the UI layer at last. This way you will be more productive and can architect your project really well.

  1. Create a ui package under lib. We will be coding the first screen i.e the Login Screen. Create a file under the ui package and name it as login.dart.


Login Screen

Copy and paste below code into the login.dart file:

import 'package:flutter/material.dart';
import 'widgets/sign_in_form.dart';

class LoginScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      padding: EdgeInsets.all(16.0),
      decoration: BoxDecoration(
          color: Colors.amber
      ),
      alignment: Alignment(0.0,0.0),
      child: SignInForm(),
    );
  }
}

In the above code I have separately created the form widget in SignInForm(). The form widget is inside another file named as sign_in_form.dart. I am breaking down a single screen into separate widgets so as to keep different widget files as simple and small as possible.

  1. Create a package under the ui directory and name it as widget. This will hold all the mini widgets of a single screen e.g the form widget of Login screen. Create the sign_in_form.dart file and paste below code in it:
import '../../utils/strings.dart';
import 'package:flutter/material.dart';
import '../../blocs/login_bloc_provider.dart';
import '../goals_list.dart';

class SignInForm extends StatefulWidget {
  @override
  SignInFormState createState() {
    return SignInFormState();
  }
}

class SignInFormState extends State<SignInForm> {
  LoginBloc _bloc;

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    _bloc = LoginBlocProvider.of(context);
  }

  @override
  void dispose() {
    _bloc.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        emailField(),
        Container(margin: EdgeInsets.only(top: 5.0, bottom: 5.0)),
        passwordField(),
        Container(margin: EdgeInsets.only(top: 5.0, bottom: 5.0)),
        submitButton()
      ],
    );
  }

  Widget passwordField() {
    return StreamBuilder(
        stream: _bloc.password,
        builder: (context, AsyncSnapshot<String> snapshot) {
          return TextField(
            onChanged: _bloc.changePassword,
            obscureText: true,
            decoration: InputDecoration(
                hintText: StringConstant.passwordHint,
                errorText: snapshot.error),
          );
        });
  }

  Widget emailField() {
    return StreamBuilder(
        stream: _bloc.email,
        builder: (context, snapshot) {
          return TextField(
            onChanged: _bloc.changeEmail,
            decoration: InputDecoration(
                hintText: StringConstant.emailHint, errorText: snapshot.error),
          );
        });
  }

  Widget submitButton() {
    return StreamBuilder(
        stream: _bloc.signInStatus,
        builder: (BuildContext context, AsyncSnapshot<bool> snapshot) {
          if (!snapshot.hasData || snapshot.hasError) {
            return button();
          } else {
            return CircularProgressIndicator();
          }
        });
  }

  Widget button() {
    return RaisedButton(
        child: Text(StringConstant.submit),
        textColor: Colors.white,
        color: Colors.black,
        shape: RoundedRectangleBorder(
            borderRadius: new BorderRadius.circular(30.0)),
        onPressed: () {
          if (_bloc.validateFields()) {
            authenticateUser();
          } else {
            showErrorMessage();
          }
        });
  }

  void authenticateUser() {
    _bloc.showProgressBar(true);
    _bloc.submit().then((value) {
      if (value == 0) {
        //New User _bloc.registerUser().then((value) {
          Navigator.pushReplacement(
              context,
              MaterialPageRoute(
                  builder: (context) => GoalsList(_bloc.emailAddress)));
        });
      } else {
        //Already registered Navigator.pushReplacement(
            context,
            MaterialPageRoute(
                builder: (context) => GoalsList(_bloc.emailAddress)));
      }
    });
  }

  void showErrorMessage() {
    final snackbar = SnackBar(
        content: Text(StringConstant.errorMessage),
        duration: new Duration(seconds: 2));
    Scaffold.of(context).showSnackBar(snackbar);
  }
}

I am using a StatefulWidget because it provides use two useful methods(initState and dispose) for initialising the bloc object and disposing all the open Streams.

  1. Now let’s build the second screen of the app. This screen will be responsible for showing our and other people goals. It look something like this:


Goal List Screen

Create a file under the ui package and name it as goals_list.dart. Copy and paste below code into the file:

import '../utils/strings.dart';
import 'package:flutter/material.dart';
import 'widgets/my_goals_list.dart';
import 'widgets/people_goals_list.dart';
import 'add_goal.dart';

class GoalsList extends StatefulWidget {
  final String _emailAddress;

  GoalsList(this._emailAddress);

  @override
  GoalsListState createState() {
    return GoalsListState();
  }
}

class GoalsListState extends State<GoalsList>
    with SingleTickerProviderStateMixin {
  TabController _tabController;

  @override
  void initState() {
    super.initState();
    _tabController = TabController(length: 2, vsync: this, initialIndex: 0);
    _tabController.addListener(_handleTabIndex);
  }

  @override
  void dispose() {
    _tabController.removeListener(_handleTabIndex);
    _tabController.dispose();
    super.dispose();
  }

  void _handleTabIndex() {
    setState(() {});
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(
          StringConstant.goalListTitle,
          style: TextStyle(
            color: Colors.black,
          ),
        ),
        backgroundColor: Colors.amber,
        elevation: 0.0,
        bottom: TabBar(
          controller: _tabController,
          tabs: <Tab>[
            Tab(text: StringConstant.worldTab),
            Tab(text: StringConstant.myTab),
          ],
        ),
      ),
      body: TabBarView(
        controller: _tabController,
        children: <Widget>[
          PeopleGoalsListScreen(),
          MyGoalsListScreen(widget._emailAddress),
        ],
      ),
      floatingActionButton: _bottomButtons(),
    );
  }

  Widget _bottomButtons() {
    if (_tabController.index == 1) {
      return FloatingActionButton(
          child: Icon(Icons.add),
          onPressed: () {
            Navigator.push(
                context,
                MaterialPageRoute(
                    builder: (context) => AddGoalScreen(widget._emailAddress)));
          });
    } else {
      return null;
    }
  }
}

In the above code I am using tab layout. I have created separate widget files for each tab. Those widgets are placed under the widgets package named as my_goals_list.dart and people_goals_list.dart.

For the first tab i.e the World tab. Copy and paste below code in the people_goals_list.dart file:

import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import '../../blocs/goals_bloc_provider.dart';
import '../../models/other_goal.dart';

class PeopleGoalsListScreen extends StatefulWidget {
  @override
  _PeopleGoalsListState createState() {
    return _PeopleGoalsListState();
  }
}

class _PeopleGoalsListState extends State<PeopleGoalsListScreen> {
  GoalsBloc _bloc;

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    _bloc = GoalsBlocProvider.of(context);
  }

  @override
  void dispose() {
    _bloc.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      alignment: Alignment(0.0, 0.0),
      child: StreamBuilder(
        stream: _bloc.othersGoalList(),
        builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
          if (snapshot.hasData) {
            List<DocumentSnapshot> docs = snapshot.data.documents;
            List<OtherGoal> goalsList = _bloc.mapToList(docList: docs);
            if (goalsList.isNotEmpty) {
              return buildList(goalsList);
            } else {
              return Text("No Goals");
            }
          } else {
            return Text("No Goals");
          }
        },
      ),
    );
  }

  ListView buildList(List<OtherGoal> goalsList) {
    return ListView.separated(
        separatorBuilder: (BuildContext context, int index) => Divider(),
        itemCount: goalsList.length,
        itemBuilder: (context, index) {
          return ListTile(
            title: Text(
              goalsList[index].title,
              style: TextStyle(
                fontWeight: FontWeight.bold,
              ),
            ),
            subtitle: Text(goalsList[index].message),
            trailing: Text(
              goalsList[index].email,
              style: TextStyle(
                fontWeight: FontWeight.bold,
                fontSize: 10.0,
              ),
            ),
          );
        });
  }
}

For the second tab i.e Me tab. Copy and paste below code in my_goals_list.dart file:

import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import '../../blocs/goals_bloc_provider.dart';
import '../../models/goal.dart';

class MyGoalsListScreen extends StatefulWidget {
  final String _emailAddress;

  MyGoalsListScreen(this._emailAddress);

  @override
  _MyGoalsListState createState() {
    return _MyGoalsListState();
  }
}

class _MyGoalsListState extends State<MyGoalsListScreen> {
  GoalsBloc _bloc;

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    _bloc = GoalsBlocProvider.of(context);
  }

  @override
  void dispose() {
    _bloc.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      alignment: Alignment(0.0, 0.0),
      child: StreamBuilder(
          stream: _bloc.myGoalsList(widget._emailAddress),
          builder:
              (BuildContext context, AsyncSnapshot<DocumentSnapshot> snapshot) {
            if (snapshot.hasData) {
              DocumentSnapshot doc = snapshot.data;
              List<Goal> goalsList = _bloc.mapToList(doc: doc);
              if (goalsList.isNotEmpty) {
                return buildList(goalsList);
              } else {
                return Text("No Goals");
              }
            } else {
              return Text("No Goals");
            }
          }),
    );
  }

  ListView buildList(List<Goal> goalsList) {
    return ListView.separated(
        separatorBuilder: (BuildContext context, int index) => Divider(),
        itemCount: goalsList.length,
        itemBuilder: (context, index) {
          final item = goalsList[index];
          return Dismissible(
              key: Key(item.id.toString()),
              onDismissed: (direction) {
                _bloc.removeGoal(item.title, widget._emailAddress);
              },
              background: Container(color: Colors.red),
              child: ListTile(
                title: Text(
                  goalsList[index].title,
                  style: TextStyle(
                    fontWeight: FontWeight.bold,
                  ),
                ),
                subtitle: Text(goalsList[index].message),
              ));
        });
  }
}

In the above screen I am using the Dismissible widget to remove a goal from my list. Just swipe left and you will get ride off that particular item.

  1. Now the last screen that we will be building is the “Add Goal Screen”. When you click on the Me tab on the home screen you will see a FloatingActionButton . Clicking the button will pop up the “Add Goal Screen”. Here how it looks:


Add Goal Screen

To make this app more exciting I added the power of Text Recognition. If you are lazy enough to type your own goal you can scan some image or note which will have the goal you were looking for. Interesting, isn’t it?

Note : I am using the official firebase_ml_vision plugin. This plugin is still under development and currently support landscape images only. Please take images in landscape mode to make the recognition work.

Create add_goal.dart file under the ui package and paste below code into it:

import 'package:flutter/material.dart';
import '../blocs/goals_bloc_provider.dart';
import 'package:image_picker/image_picker.dart';

class AddGoalScreen extends StatefulWidget {
  final String _emailAddress;

  AddGoalScreen(this._emailAddress);

  @override
  AddGoalsState createState() {
    return AddGoalsState();
  }
}

class AddGoalsState extends State<AddGoalScreen> {
  GoalsBloc _bloc;
  TextEditingController myController = TextEditingController();

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    _bloc = GoalsBlocProvider.of(context);
  }

  @override
  void dispose() {
    myController.dispose();
    _bloc.dispose();
    super.dispose();
  }

  //Handing back press Future<bool> _onWillPop() {
    Navigator.pop(context, false);
    return Future.value(false);
  }

  @override
  Widget build(BuildContext context) {
    return WillPopScope(
      onWillPop: _onWillPop,
      child: Scaffold(
        appBar: AppBar(
          title: Text(
            "Add Goal",
            style: TextStyle(color: Colors.black),
          ),
          backgroundColor: Colors.amber,
          elevation: 0.0,
        ),
        body: Container(
          padding: EdgeInsets.all(16.0),
          alignment: Alignment(0.0, 0.0),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              nameField(),
              Container(margin: EdgeInsets.only(top: 5.0, bottom: 5.0)),
              goalField(),
              Container(margin: EdgeInsets.only(top: 5.0, bottom: 5.0)),
              buttons(),
            ],
          ),
        ),
      ),
    );
  }

  Widget nameField() {
    return StreamBuilder(
        stream: _bloc.name,
        builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
          return TextField(
            onChanged: _bloc.changeName,
            decoration: InputDecoration(
                hintText: "Enter name", errorText: snapshot.error),
          );
        });
  }

  Widget goalField() {
    return StreamBuilder(
        stream: _bloc.goalMessage,
        builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
          myController.value = myController.value.copyWith(text: snapshot.data);
          return TextField(
            controller: myController,
            keyboardType: TextInputType.multiline,
            maxLines: 3,
            onChanged: _bloc.changeGoalMessage,
            decoration: InputDecoration(
                hintText: "Enter your goal here", errorText: snapshot.error),
          );
        });
  }

  Widget buttons() {
    return StreamBuilder(
        stream: _bloc.showProgress,
        builder: (BuildContext context, AsyncSnapshot<bool> snapshot) {
          if (!snapshot.hasData) {
            return Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                submitButton(),
                Container(margin: EdgeInsets.only(left: 5.0, right: 5.0)),
                scanButton(),
              ],
            );
          } else {
            if (!snapshot.data) {
              //hide progress bar return Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: <Widget>[
                  submitButton(),
                  Container(margin: EdgeInsets.only(left: 5.0, right: 5.0)),
                  scanButton(),
                ],
              );
            } else {
              return CircularProgressIndicator();
            }
          }
        });
  }

  Widget submitButton() {
    return RaisedButton(
        textColor: Colors.white,
        color: Colors.black,
        child: Text("Submit"),
        shape: RoundedRectangleBorder(
            borderRadius: new BorderRadius.circular(30.0)),
        onPressed: () {
          _bloc.submit(widget._emailAddress);
        });
  }

  Widget scanButton() {
    return RaisedButton.icon(
        icon: Icon(Icons.add_a_photo),
        label: Text("Scan Goal"),
        textColor: Colors.white,
        color: Colors.black,
        shape: RoundedRectangleBorder(
            borderRadius: new BorderRadius.circular(30.0)),
        onPressed: () {
          getImage();
        });
  }

  void getImage() async {
    var image = await ImagePicker.pickImage(source: ImageSource.camera);
    _bloc.extractText(image);
  }
}

In the above code there is a method named as getImage(). In that I am using the ImagePicker plugin to open up the camera and then passing the image to the ml_vision for text extraction.

  1. I guess we are almost done building the app. Just one last thing! I have created a strings.dart file which will hold all the strings used in the app.

Create a package named as utils under the lib directory. Create strings.dart file under it and paste the below code:

class StringConstant{
  static const String goalValidateMessage = "Your goal should have min of 10 characters";
  static const String nameValidateMessage = "Only alphabets are allowed";
  static const String emailField = "email";
  static const String goalField = "goals";
  static const String collectionName = "users";
  static const String emailValidateMessage = "Enter a valid email";
  static const String passwordValidateMessage = "Password must be at least 4 characters";
  static const String passwordHint = "Enter Password";
  static const String emailHint = "Enter Email ID";
  static const String submit = "Submit";
  static const String errorMessage = "Please fix all the errors";
  static const String goalListTitle = "Goals List";
  static const String worldTab = "World";
  static const String myTab = "Me";

}

Uff! So much to take in. But trust me if you made it this far you have learnt a lot and now you are ready to architect your own Flutter project using the BLoC pattern. You will find the link of this project here.

Before leaving let’s see a small demo of what we have built today:

That’s all I have for today. I hope everyone must have got something out of this article. If you have any queries or questions connect with me at LinkedIn or Twitter. Thanks for showing interest in my work. Love you all 😃 ❤️


The Flutter Pub is a medium publication to bring you the latest and amazing resources such as articles, videos, codes, podcasts etc. about this great technology to teach you how to build beautiful apps with it. You can find us on Facebook, Twitter, and Medium or learn more about us here. We’d love to connect! And if you are a writer interested in writing for us, then you can do so through these guidelines.

Discover and read more posts from sagar suri
get started
post commentsBe the first to share your opinion
Show more replies