15
Adding State Management to the Firestore Client App

WHAT YOU WILL LEARN IN THIS CHAPTER

  • How to use state management to control Firebase Authentication and the Cloud Firestore database
  • How to use the BLoC pattern to separate business logic
  • How to use the InheritedWidget class as a provider to manage and pass state
  • How to implement abstract classes
  • How to use StreamBuilder to receive the latest data from Firebase Authentication and the Cloud Firestore database
  • How to use StreamController, Stream, and Sink to handle Firebase Authentication and Cloud Firestore data events
  • How to create service classes to handle Firebase Authentication and Cloud Firestore API calls with Stream and Future classes
  • How to create a model class for individual journal entries and convert Cloud Firestore QuerySnapshot and map it to the Journal class
  • How to use the optional Firestore Transaction to save data to the Firestore database
  • How to create a class to handle mood icons, descriptions, and rotation
  • How to create a class to handle date formatting
  • How to use the ListView.separated named constructor

In this chapter, you'll continue to edit the mood journaling app created in Chapter 14. For your convenience, you can use the ch14_final_journal project as your starting point and make sure you add your GoogleService‐Info.plist file to the Xcode project and the google‐services.json file to the Android project that you downloaded in Chapter 14 from your Firebase console.

You'll learn how to implement app‐wide and local‐state management that uses the InheritedWidget class as a provider to manage and pass State between widgets and pages.

You'll learn how to use the Business Logic Component (BLoC) pattern to create BLoC classes, for example managing access to the Firebase Authentication and Cloud Firestore database service classes. You'll learn how to use a reactive approach by using StreamBuilder, StreamController, and Stream to populate and refresh data.

You'll learn how to create a service class to manage the Firebase Authentication API by implementing an abstract class that manages the user login credentials. You'll create a separate service class to handle the Cloud Firestore database API. You'll learn how to create a Journal model class to handle the mapping of the Cloud Firestore QuerySnapshot to individual records. You'll learn how to create a mood icons class to manage a list of mood icons, a description, and an icon rotation position according to the selected mood. You'll learn how to create a date formatting class using the intl package.

IMPLEMENTING STATE MANAGEMENT

Before you dive into state management, let's take a look at what state means. At its most basic, state is data that is read synchronously and can change over time. For example, a Text widget value is updated to show the latest game score, and the state for the Text widget is the value. State management is the way to share data (state) between pages and widgets.

You can have app‐wide state management to share the state between different pages. For example, the authentication state manager monitors the logged‐in user and when the user logs out, it takes the appropriate action to redirect to the login page. Figure 15.1 shows the home page getting the state from the main page; this is app‐wide state management.

Schematic of App-wide state management.

FIGURE 15.1: App‐wide state management

You can have local‐state management confined to a single page or a single widget. For example, the page displays a selected item, and the purchase button needs to be enabled only if the item is in‐stock. The button widget needs to access the state of the in‐stock value. Figure 15.2 shows an Add button getting state up the widget tree; this is local‐state management.

Schematic of local-state management.

FIGURE 15.2: Local‐state management

There are many different techniques for handling state management, and there isn't a right or wrong answer on which approach to take because it depends on your needs and personal preference. The beauty is that you can create a custom approach to state management. You have already mastered one of the state‐management techniques, the setState() method. In Chapter 2 you learned how to use the StatefulWidget and call the setState() method to propagate changes to the UI. Using the setState() method is the default way that a Flutter app manages state changes, and you have used it for all of the example apps that you've created from this book.

To show different state management approaches, the journal app uses a combination of an abstract class and an InheritedWidget class as providers, plus a service class, a mood utility class, a date utility class, and the BLoC pattern to separate the business code logic from the UI.

Implementing an Abstract Class

One of the main benefits of using an abstract class is to separate the interface methods (called from the UI) from the actual code logic. In other words, you declare the methods without any implementation (code). Another benefit is that the abstract class cannot be directly instantiated, meaning an object cannot be created from it unless you define a factory constructor. Abstract classes help you to program to interfaces, not the implementation. Concrete classes implement the methods of the abstract class.

By default (concrete) classes define an interface containing all of the members and methods that it implements. The following example shows the AuthenticationService class declaring a variable and methods containing the code logic:

class AuthenticationService {
 final FirebaseAuth _firebaseAuth = FirebaseAuth.instance;

 Future<void> sendEmailVerification() async {
  FirebaseUser user = await _firebaseAuth.currentUser();
  user.sendEmailVerification();
 }

 Future<bool> isEmailVerified() async {
  FirebaseUser user = await _firebaseAuth.currentUser();
  return user.isEmailVerified;
 }
}

You are going to use an abstract class to define your authentication interface in this section. The abstract class has callable methods without containing the actual code (implementation), and they are called abstract methods. To declare an abstract class, you use the abstract modifier before the class declaration like abstract class Authentication {}. The abstract methods work with a class that implements one or more interfaces and is declared by using the implements clause like this example: class AuthenticationService implements Authentication {}. Note that the abstract method is declared by using a semicolon (;) instead of the body declared by curly brackets ({}). The code logic for the abstract methods is implemented (concrete implementation) in the class that implements the abstract class.

The following example declares the Authentication class as an abstract class by using the abstract modifier and contains two abstract methods. The AuthenticationService class uses the implements clause to implement the methods declared by the Authentication class.

abstract class Authentication {
 Future<void> sendEmailVerification();
 Future<bool> isEmailVerified();
}

class AuthenticationService implements Authentication {
 final FirebaseAuth _firebaseAuth = FirebaseAuth.instance;

 Future<void> sendEmailVerification() async {
  FirebaseUser user = await _firebaseAuth.currentUser();
  user.sendEmailVerification();
 }

 Future<bool> isEmailVerified() async {
  FirebaseUser user = await _firebaseAuth.currentUser();
  return user.isEmailVerified;
 }
}

Why use an abstract class instead of declaring a class with variables and methods? Of course, you can use the class without creating an abstract class for the interface since the class already declares them by default. But one of the benefits of using an abstract class is to impose implementation and design constraints.

For the journal app, the main benefits of using abstract classes are to use them in the BLoC classes, and with dependency injection you inject the platform‐dependent implementation classes, making the BLoC classes platform‐agnostic. Dependency injection is a way to make a class independent of its dependencies. The class does not contain platform‐specific code (libraries), but it is passed (injected) at runtime. You'll learn about the BLoC pattern in the “Implementing the BLoC Pattern” section of this chapter.

Implementing the InheritedWidget

One of the ways to pass State between pages and the widget tree is to use the InheritedWidget class as a provider. A provider holds an object and provides it to its child widgets. For example, you use the InheritedWidget class as the provider of a BLoC class responsible for making API calls to the Firebase Authentication API. As a reminder, the BLoC pattern is covered in the “Implementing the BLoC Pattern” section. In the example we'll go through now, I cover how to use the InheritedWidget class with a BLoC class, but you could also use it with a regular service class instead. You'll learn how to create service classes in the “Implementing the Service Class” section.

For the journal app, the relationship between the InheritedWidget class and the BLoC class is one to one, meaning one InheritedWidget class for one BLoC class. You'll use the of(context) to get a reference to the BLoC class; for example, AuthenticationBlocProvider.of(context).authenticationBloc.

The following example shows the AuthenticationBlocProvider class that extends (subclasses) the InheritedWidget class. The BLoC authenticationBloc variable is marked as final, which references the AuthenticationBloc BLoC class. The AuthenticationBlocProvider constructor takes a Key, Widget, and this.authenticationBloc variable.

// authentication_provider.dart
class AuthenticationBlocProvider extends InheritedWidget {
 final AuthenticationBloc authenticationBloc;
 const AuthenticationBlocProvider({Key key, Widget child, this.authenticationBloc}) : super(key: key, child: child);

 static AuthenticationBlocProvider of(BuildContext context) {
  return (context.inheritFromWidgetOfExactType(AuthenticationBlocProvider) as AuthenticationBlocProvider);
 }

 @override
 bool updateShouldNotify(AuthenticationBlocProvider old) => authenticationBloc != old.authenticationBloc;
}

To access the AuthenticationBlocProvider class from a page, you use the of() method. When a page loads, the InheritedWidget needs to be called from the didChangeDependencies method, not the initState method. If the inherited values change, they would not be called again from the initState, but to make sure the widget updates when the values change, you need to use the didChangeDependencies method.

// page.dart
@override
void didChangeDependencies() {
 super.didChangeDependencies();
 _authenticationBloc = AuthenticationBlocProvider.of(context).authenticationBloc;
}

// Logout a user from a button widget
_authenticationBloc.logoutUser.add(true);

Implementing the Model Class

The model class is responsible for modeling the data structure. The data model represents the data structure for how the data is stored in the database or data storage. The data structure declares the data type for each variable as a String or Boolean. You can also implement methods that perform a specific function like mapping data from one format to another.

The following example shows a model class declaring the data structure and a method to map and convert data:

class Journal {
 String documentID;
 String date;

 Journal({
  this.documentID,
  this.date
 });

 factory Journal.fromDoc(dynamic doc) => Journal(
   documentID: doc.documentID,
   date: doc["date"]
 );
}

Implementing the Service Class

The journal app uses Firebase Authentication to verify user credentials and the Cloud Firestore database for storing data to the cloud. The services are invoked by making the appropriate API call.

Creating a class to group all of the same type services together is a great option. Another benefit of separating services in service classes is that this makes it easier to create separate classes to implement additional or alternative services. For the journal app, you'll learn how to implement the service classes as abstract classes, but for this example, I wanted to show you how to implement the basic service class.

The following example shows a DbFirestoreService class that implements methods to call the Cloud Firestore database API:

class DbFirestoreService {
 Firestore _firestore = Firestore.instance;
 String _collectionJournals = 'journals';

 DbFirestoreService() {
  _firestore.settings(timestampsInSnapshotsEnabled: true);
 }

 Stream<List<Journal>> getJournalList(String uid) {
  return _firestore
    .collection(_collectionJournals)
    .where('uid', isEqualTo: uid)
    .snapshots()
    .map((QuerySnapshot snapshot) {
   List<Journal> _journalDocs = snapshot.documents.map((doc) => Journal.fromDoc(doc)).toList();
   _journalDocs.sort((comp1, comp2) => comp2.date.compareTo(comp1.date));
   return _journalDocs;
  });
 }

 Future<bool> addJournal(Journal journal) async {}
 void updateJournal(Journal journal) async {}
 void updateJournalWithTransaction(Journal journal) async {}
 void deleteJournal(Journal journal) async {}
}

Implementing the BLoC Pattern

BLoC stands for Business Logic Component, and it was conceived to define a platform‐agnostic interface for the business logic. The BLoC pattern was developed internally by Google to maximize code sharing between Flutter mobile and AngularDart web apps. It was first publicly presented by Paolo Soares at the DartConf 2018 conference. The BLoC pattern exposes streams and sinks to handle data flow, making the pattern reactive. In its simplest form, reactive programming handles the data flow with asynchronous streams and the propagation of data changes. The BLoC pattern omits how the data store is implemented; that is up to the developer to choose according to project requirements. By separating the business logic, it does not matter which infrastructure you use to build your app; other parts of the app can change, and the business logic remains intact.

Paolo Soares shared the following BLoC pattern guidelines at the DartConf 2018 conference.

The BLoC pattern has design guidelines to adhere to:

  • Inputs and outputs are Streams and Sinks only
  • Platform agnostic and dependencies must be injectable
  • No platform branching allowed
  • Implementation is up to the developer like reactive programming

The BLoC pattern has UI design guidelines to adhere to:

  • Create a BLoC for each complex enough component
  • Components should send inputs as is
  • Components should show outputs as close as possible to as is
  • All branching should be based on simple BLoC Boolean outputs

You'll learn to use the InheritedWidget as a provider to access and pass the BLoC classes between pages. You'll also learn how to instantiate a BLoC without a provider for pages that do not require sharing a reference between them—for example, the login page.

The following summarizes the BLoC pattern guidelines presented at the DartConf 2018 conference.

  • Move business logic to BLoCs
  • Keep UI components simple
  • Design rules aren't negotiable

Let's take a look at a BLoC class structure. Although it's not required, I name the class with a descriptive name and the word Bloc like HomeBloc. The following example HomeBloc class handles the database calls to the DbFirestoreService API service class, retrieves the list of converted journal entries, and then sends it back to the widget (UI). The DbFirestoreService is injected to the HomeBloc() constructor, making it platform independent. In the HomeBloc class, the DbApi abstract class is platform independent and receives the injected DbFirestoreService class. The Business Logic Component processes, formats, and sends the output back to the widget, and the receiving client app could be mobile, web, or desktop, resulting in business logic separation and maximum code sharing between platforms.

The following example shows how the platform‐dependent DbFirestoreService() class is injected to the HomeBloc(DbFirestoreService()) constructor, resulting in the HomeBloc() class remaining platform independent.

// Inject the DbFirestoreService() to the HomeBloc() from UI widgets page
// by using dependency injection
HomeBloc(DbFirestoreService());

// BLoC pattern class
// The HomeBloc(this.dbApi) constructor receives 
// the injected DbFirestoreService() classclass HomeBloc {
 final DbApi dbApi;

 final StreamController<List<Journal>> _journalController = StreamController<List<Journal>>();
 Sink<List<Journal>> get _addJournal => _journalController.sink;
 Stream<List<Journal>> get listJournal => _journalController.stream;

 // Constructor
 HomeBloc(this.dbApi) {
  _startListeners();
 }

 // Close StreamControllers when no longer needed
 void dispose() {
  _journalController.close();
 }

 void _startListeners() {
  // Retrieve Firestore Journal Records as List<Journal> not DocumentSnapshot
  dbApi.getJournalList().listen((journalDocs) {
   _addListJournal.add(journalDocs);
  });
 }
}

Implementing StreamController, Streams, Sinks, and StreamBuilder

The StreamController is responsible for sending data, done events, and errors on the stream property. The StreamController has a sink (input) property and a stream (output) property. To add data to the stream, you use the sink property, and to receive data from the stream, you listen to the stream events by setting up listeners. The Stream class is asynchronous data events, and the Sink class allows the adding of data values to the StreamController stream property.

The following example shows how to use the StreamController class. You use the Sink class to add data with the sink property and the Stream class to send data with the stream property of the StreamController. Note the use of the get keyword for the _addUser (Sink) and the user (Stream) declarations. The get keyword is called a getter, and it's a special method that provides read and write access to the object's properties.

final StreamController<String> _authController = StreamController<String>();
Sink<String> get _addUser => _authController.sink;
Stream<String> get user => _authController.stream;

To add data to the stream property, you use the sink property's add(event) method.

_addUser.add(mood);

To listen to stream events, you use the listen() method to subscribe to the stream. You also use the StreamBuilder widget to listen for stream events.

_authController.listen((mood) {
 print('My mood: $mood');
})

When you need multiple listeners to the StreamController stream property, use the StreamController broadcast constructor. For example, you might have a StreamBuilder widget and a listener listening to the same StreamController class.

final StreamController<String> _authController = StreamController<String>.broadcast();

The StreamBuilder widget rebuilds itself based on the latest snapshot of new events from a Stream class, and you'll use it to build your reactive widgets to display data. In other words, the StreamBuilder rebuilds every time it receives a new event from a stream.

StreamBuilder(
 initialData: '',
 stream: user,
 builder: (BuildContext context, AsyncSnapshot snapshot) {
  return Text('Hello $snapshot.data');
 },
)

Use the initialData property to set the initial data snapshot before the stream sends the latest data events. The builder is always called before the stream listener has a chance to process data, and by setting the initialData, a default value is shown instead of a blank value.

initialData: '',

The stream property is set to the stream responsible for the latest data events, for example, the StreamController stream property.

stream: user,

The builder property is used to add your logic to build a widget according to the results of the stream data events. The builder property takes the BuildContext and AsyncSnapshot parameters. The AsyncSnapshot contains the connection and data information that you learned in Chapter 13's “Retrieving Data with the FutureBuilder” section. Make sure that the builder returns a widget; otherwise, you'll receive an error that the build functions returned a null. In Flutter the build functions must never return a null value.

builder: (BuildContext context, AsyncSnapshot snapshot) {
 return Text('Hello $snapshot.data');
},

The following example shows how to use the StreamBuilder widget to reactively change the UI widget depending on the stream property value. This reactive programming results in performance gain since only this widget in the widget tree rebuilds to redraw a new value when the stream changes. Note that the user stream is configured from the previous StreamController example.

StreamBuilder(
 initialData: '',
 stream: user,
 builder: (BuildContext context, AsyncSnapshot snapshot) {
  if (snapshot.connectionState == ConnectionState.waiting) {
   return Container(color: Colors.yellow,);
  } else if (snapshot.hasData) {
   return Container(color: Colors.lightGreen);
  } else {
   return Container(color: Colors.red);
  }
 },
),

BUILDING STATE MANAGEMENT

Before implementing state management for the client journal app that you created in Chapter 14, let's go over the overall plan and priority steps. In order of creation, you'll create the model class, service classes, utility classes, validator classes, BLoC classes, and InheritedWidget class as a provider, and finally you'll add state management and BLoCs for all pages. You begin by modifying the main page, creating the login page, modifying the home page, and creating the entry page.

Note that from the UI widget pages you'll inject the platform‐specific authentication.dart and db_firestore.dart service classes to the BLoC class constructor. The BLoC class uses the API abstract class to receive the injected platform‐specific service class, making the BLoC class platform‐agnostic. If you were also creating a web version of the journal app, you would inject the web appropriate authentication and database service classes to the BLoC classes, and they would just work; these are some the benefits of using the BLoC pattern.

Table 15.1 lists the folders and pages structure for the journal app.

TABLE 15.1: Folders and files structure

FOLDERS FILES
blocs authentication_bloc.dart
authentication_bloc_provider.dart
home_bloc.dart
home_bloc_provider.dart
journal_edit_bloc.dart
journal_edit_bloc_provider.dart
login_bloc.dart
classes format_dates.dart
mood_icons.dart
validators.dart
models journal.dart
pages edit_entry.dart
home.dart
login.dart
services authentication.dart
authentication_api.dart
db_firestore.dart
db_firestore_api.dart
Root folder main.dart

To help you visualize the app that you are developing, Figure 15.3 shows the final design for the mood journaling app. From left to right, it shows the login page, home page, journal‐entry deletion, and edit entry page.

Screenshot of the final mood journal app.

FIGURE 15.3: The final mood journal app

Adding the Journal Model Class

For the journal app, you'll create a Journal model class that is responsible for holding individual journal entries and mapping a Cloud Firestore document to a Journal entry. The Journal class holds the documentID, date, mood, note, and uid String fields. The documentID variable stores a reference to the Cloud Firestore database document unique ID. The uid variable stores the logged‐in user unique ID. The date variable is formatted as an ISO 8601 standard, for example, 2019‐03‐18T13:56:54.985747. The mood variable stores the mood name like Satisfied, Neutral, and so on. The note variable stores the detailed journal description for the entry.

Adding the Service Classes

They are called service classes because they send and receive calls to a service. The journal app has two service classes to handle the Firebase Authentication and Cloud Firestore database API calls.

The AuthenticationService class implements the AuthenticationApi abstract class. The DbFirestoreService class implements the DbApi abstract class. The following is a sample call to the Cloud Firestore database to query records; Table 15.2 describes the details:

TABLE 15.2: How to query the database

CALL DESCRIPTION
Firestore.instance Obtain the Firestore.instance reference.
.collection('journals') Specify the collection name.
.where('uid', isEqualTo: uid) The where() method filters by the specified field.
.snapshots() The snapshots() method returns a Stream of a QuerySnapshot containing the record(s).
Firestore.instance
 .collection("journals")
 .where('uid', isEqualTo: uid)
 .snapshots() 

Cloud Firestore supports using transactions. One of the benefits of using transactions is to group multiple operations (add, update, delete) in one transaction. Another case is concurrent editing: when multiple users are editing the same record, the transaction is run again, making sure the latest data is used before updating. If one of the operations fails, the transaction will not do a partial update. However, if the transaction is successful, all of the updates are executed.

The following is a sample transaction that takes the _docRef document reference and calls the runTransaction() method to update the document's data:

DocumentReference _docRef = _firestore.collection('journals').document('Cf409us32');
var journalData = {
 'date': journal.date,
 'mood': journal.mood,
 'note': journal.note,
};
_firestore.runTransaction((transaction) async {
 await transaction
   .update(_docRef, journalData)
   .catchError((error) => print('Error updating: $error'));
});

Adding the Validators Class

The Validators class uses the StreamTransformer to validate whether the email is in the correct format by using at least one @ sign and a period. The password validator checks for a minimum of six characters entered. The Validators class is used with the BLoC classes.

The StreamTransformer transforms a Stream that is used to validate and process values inside a Stream. The incoming data is a Stream, and the outgoing data after processing is a Stream. For example, once the incoming data is processed, you can use the sink.add() method to add data to the Stream or use the sink.addError() method to return a validation error. The StreamTransformer.fromHandlers constructor is used to delegate events to a given function.

The following is an example that shows how to use the StreamTransformer by using the fromHandlers constructor to validate whether the email is in the correct format:

StreamTransformer<String, String>.fromHandlers(handleData: (email, sink) {
 if (email.contains('@') && email.contains('.')) {
  sink.add(email);
 } else if (email.length > 0) {
  sink.addError('Enter a valid email');
 }
});

Adding the BLoC Pattern

In this section, you'll create the authentication BLoC, authentication BLoC provider, login BLoC, home BLoC, home BLoC provider, journal edit BLoC, and journal edit BLoC provider. The login BLoC doesn't need a provider class because it does not rely on receiving data from other pages.

I want to remind you of this important concept: BLoC classes are platform‐agnostic and do not rely on platform‐specific packages or classes. For example, the Login page injects the platform‐specific (Flutter) AuthenticationService class to the LoginBloc class constructor. The receiving BLoC class has the abstract AuthenticationApi class that receives the injected AuthenticationService class, making the BLoC class platform‐agnostic.

Adding the AuthenticationBloc

The AuthenticationBloc is responsible for identifying logged‐in user credentials and monitoring user authentication login status. When the AuthenticationBloc is instantiated, it starts a StreamController listener that monitors the user's authentication credentials, and when changes occur, the listener updates the credential status by calling a sink.add() method event. If the user is logged in, the sink events send the user uid value, and if the user logs out, the sink events sends a null value, meaning no user is logged in.

Adding the AuthenticationBlocProvider

The AuthenticationBlocProvider class is responsible for passing the State between widgets and pages by using the InheritedWidget class as a provider. The AuthenticationBlocProvider constructor takes a Key, Widget, and the this.authenticationBloc variable, which is the AuthenticationBloc class.

Adding the LoginBloc

The LoginBloc is responsible for monitoring the login page to check for a valid email format and password length. When the LoginBloc is instantiated, it starts the StreamController's listeners that monitor the user's email and password, and once they pass validation, the login and create account buttons are enabled. Once the login and password values pass validation, the authentication service is called to log in or create a new user. The Validators class is responsible for validating the email and password values.

Adding the HomeBloc

The HomeBloc is responsible for identifying logged‐in user credentials and monitoring user authentication login status. When the HomeBloc is instantiated, it starts a StreamController listener that monitors the user's authentication credentials, and when changes occur, the listener updates the credential status by calling a sink.add() method event. If the user is logged in, the sink events send the user uid value, and if the user logs out, the sink events send a null value, meaning no user is logged in.

Adding the HomeBlocProvider

The HomeBlocProvider class is responsible for passing the State between widgets and pages by using the InheritedWidget class as a provider. The HomeBlocProvider constructor takes a Key, Widget, and the this.authenticationBloc variable, which is the HomeBloc class.

Adding the JournalEditBloc

The JournalEditBloc is responsible for monitoring the journal edit page to either add a new or save an existing entry. When the JournalEditBloc is instantiated, it starts the StreamController's listeners that monitor the date, mood, note, and save button streams.

Adding the JournalEditBlocProvider

The JournalEditBlocProvider class is responsible for passing the State between widgets and pages by using the InheritedWidget class as a provider. The JournalEditBlocProvider constructor takes a Key, Widget, and this.journalEditBloc variables. Note that the this.journalEditBloc variable is the JournalEditBloc class.

SUMMARY

In this chapter, you implemented the client app's app‐wide and local‐state management. You learned how to implement the BLoC pattern to separate business logic from the UI pages. You created an abstract class to define the authentication interface and the authentication service class to implement the abstract class. By using the abstract class, you can impose implementation and design constraints. You used the abstract class with the BLoC class to inject at runtime the appropriate platform‐dependent class, resulting in the BLoC class being platform‐agnostic.

You implemented an InheritedWidget class as a provider to pass State between widgets and pages. You used the of() method to access reference to the provider. You created the Journal model class to structure the individual journal records and used the fromDoc() method to convert and map a Cloud Firestore database document to an individual Journal entry. You created service classes to manage sending and receiving the service API calls. You created the AuthenticationService class implementing the AuthenticationApi abstract class to access the Firebase Authentication API. You created the DbFirestoreService class implementing the DbApi abstract class to access the Cloud Firestore database API.

You implemented the BLoC pattern to maximize separation of the UI widgets and the business logic components. You learned that the pattern exposes sinks to input data and exposes streams to output data. You learned how to inject the platform‐aware service classes to the BLoC's constructor, making the BLoC classes platform independent. By separating the business logic from the UI, it does not matter if you are using Flutter for the mobile apps, AngularDart for web apps, or any other platform. You implemented the StreamController to send data, done events, and errors on the stream property. You implemented the Sink class to add data with the sink property and the Stream class to send data with the stream property of the StreamController.

In the next chapter, you'll learn how to implement reactive pages to communicate with the BLoCs. You'll modify the main page to implement the app‐wide state management by using the authentication BLoC provider. You'll create the login page and implement the BLoC to validate the email and password, log in, and create a new user account. You'll modify the home page to implement the database BLoC and use the ListView.separated constructor. You'll create the journal edit entry page and implement the BLoC to create and update existing entries.

image WHAT YOU LEARNED IN THIS CHAPTER

TOPIC KEY CONCEPTS
App‐wide and local‐state management You learned how to apply state management by creating the InheritedWidget class as a provider to pass State between widgets and pages.
abstract class You learned how to implement an abstract class and abstract methods to define the authentication interface. You created the AuthenticationService class that implements the abstract class.
Model class You learned how to create the Journal model class responsible for modeling the data structure and map Cloud Firestore database QuerySnapshot to individual Journal entries.
Service classes You learned how to create service classes that call different services API. You created the AuthenticationService class to call the Firebase Authentication API and the DbFirestoreService class to call the Cloud Firestore database API.
Validator class You learned how to create the Validators class that uses the StreamTransformer to validate the email and password to pass minimum requirements.
StreamController, Streams, Sinks, and StreamBuilder You learned how to use the StreamController to send data, done events, and errors on the stream property. The StreamController has a sink (input) property and a stream (output) property.
BLoC pattern The BLoC acronym stands for Business Logic Component, and it was created to define a platform‐agnostic interface for the business logic. In other words, it separates the business logic from the UI widgets/components.
BLoC classes You learned how to create the AuthenticationBloc, LoginBloc, HomeBloc, and JournalEditBloc BLoC classes.
BLoC dependency injection You learned how to use abstract classes with the BLoC classes and how to inject platform‐dependent classes to the BLoC classes, making the BLoC classes platform‐agnostic.
InheritedWidget class as a provider You learned how to create the AuthenticationBlocProvider, HomeBlocProvider, and JournalEditBlocProvider classes that extend the InheritedWidget class to act as providers for the AuthenticationBloc, HomeBloc, and JournalEditBloc classes.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset