CHAPTER 5
We have reached an important milestone—our app’s components are ready. However, there are two important parts of our app’s logic we are missing: the model and service provider. In this chapter, we are going to create our application’s model.
The application’s model is going to contain the object definition of the data that will be stored in Firebase (along with some utility methods). The model is going to be used by the service provider that will be responsible for fetching, inserting, updating, and deleting documents.
In VS Code, go to the model folder within your app’s project and open the docs.dart file previously created. Once this file is open, copy and paste the code from the following listing.
Code Listing 5-a: docs.dart (Full Code)
import 'dart:convert'; import 'dart:math'; class utils { static final Random _random = Random.secure(); static String CreateRandomString([int length = 32]) { var values = List<int>.generate( length, (i) => _random.nextInt(256)); return base64Url.encode(values); } } class Doc { List<String> months = [ 'Jan|01', 'Feb|02', 'Mar|03', 'Apr|04', 'May|05', 'Jun|06', 'Jul|07', 'Aug|08', 'Sep|09', 'Oct|10', 'Nov|11', 'Dec|12' ];
String ID; String Document; String Year; String Month; String Day; String Expires; String Status; String Badge; bool Edit; bool Alert12; bool Alert6; bool Alert3; bool Alert1; int Days; Doc({ this.ID, this.Document, this.Year, this.Month, this.Day, this.Expires, this.Status, this.Badge, this.Edit = true, this.Alert12, this.Alert6, this.Alert3, this.Alert1, this.Days }); String getMonthNum(String m) { var n = ''; for (var i = 0; i < months.length; i++) { if (months[i].contains(m)) { var p = months[i].split('|'); n = p[1]; break; } } return n; } List determineStatus(String y, String m, String d) { var r = ''; var badge = ''; var exp = DateTime(int.parse(y), int.parse(getMonthNum(m)), int.parse(d)); var diffDays = exp.difference(DateTime.now()).inDays; if (diffDays <= 0) { r = 'Expired'; r = r.replaceAll('-', ''); badge = 'badge badge-dark ml-2'; } else if (diffDays > 0 && diffDays <= 30) { r = diffDays.toString() + ' day(s)'; badge = 'badge badge-danger ml-2'; } else if (diffDays > 30 && diffDays <= 60) { r = diffDays.toString() + ' day(s)'; badge = 'badge badge-warning ml-2'; } else if (diffDays > 60 && diffDays <= 90) { r = diffDays.toString() + ' day(s)'; badge = 'badge badge-secondary ml-2'; } else if (diffDays > 90 && diffDays <= 120) { r = diffDays.toString() + ' day(s)'; badge = 'badge badge-info ml-2'; } else if (diffDays > 120 && diffDays <= 240) { r = diffDays.toString() + ' day(s)'; badge = 'badge badge-primary ml-2'; } else if (diffDays > 240 && diffDays <= 365) { r = diffDays.toString() + ' day(s)'; badge = 'badge badge-default ml-2'; } else if (diffDays > 365) { r = diffDays.toString() + ' day(s)'; badge = 'badge badge-success ml-2'; } return [r, diffDays, badge]; } void update(Doc doc) { if (doc.ID == '-1') { ID = utils.CreateRandomString(); } Document = doc.Document; Year = doc.Year; Month = doc.Month; Day = doc.Day; Expires = doc.Day + '-' + doc.Month + '-' + doc.Year; var st = determineStatus(Year, Month, Day); Status = st[0]; Badge = st[2];
Alert12 = doc.Alert12; Alert6 = doc.Alert6; Alert3 = doc.Alert3; Alert1 = doc.Alert1; Days = st[1]; } static Function(Doc, Doc) sorter(int sortOrder, String property) { int handleSortOrder(int sortOrder, int sort) { if (sortOrder == 1) { // a is before b if (sort == -1) { return -1; } else if (sort > 0) { // a is after b return 1; } else { // a is same as b return 0; } } } return (Doc a, Doc b) { if (a != null && b != null) { switch (property) { case 'Days': var sort = a.Days.compareTo(b.Days); return handleSortOrder(sortOrder, sort); default: break; } } }; } // sortOrder = 1 ascending | 0 descending static void sortDocs(List<Doc> docs, {int sortOrder = 1, String property = 'Days'}) { if (docs.isNotEmpty) { switch (property) { case 'Days': docs.sort(sorter(sortOrder, property)); break; default: print('Unrecognized property $property'); } } } // Firebase Doc.fromMap(Map mp) : this ( ID: mp['ID'].toString(), Document: mp['Document'].toString(), Year: mp['Year'].toString(), Month: mp['Month'].toString(), Day: mp['Day'].toString(), Expires: mp['Expires'].toString(), Status: mp['Status'].toString(), Badge: mp['Badge'].toString(), Edit: mp['Edit']. toString(). toLowerCase() == 'true' ?? false, Alert12: mp['Alert12']. toString(). toLowerCase() == 'true' ?? false, Alert6: mp['Alert6']. toString(). toLowerCase() == 'true' ?? false, Alert3: mp['Alert3']. toString(). toLowerCase() == 'true' ?? false, Alert1: mp['Alert1']. toString(). toLowerCase() == 'true' ?? false, Days: int.parse( mp['Days'].toString()) ); // Firebase Map asMap() => { 'ID': ID, 'Document': Document, 'Year': Year, 'Month': Month, 'Day': Day, 'Expires': Expires, 'Status': Status, 'Badge': Badge, 'Edit': Edit, 'Alert12': Alert12, 'Alert6': Alert6, 'Alert3': Alert3, 'Alert1': Alert1, 'Days': Days }; } |
To understand what is happening here, let’s dissect the code into smaller parts. We start by importing the modules we need.
import 'dart:convert';
import 'dart:math';
We’ll be using Dart’s convert and math standard libraries, which are key to converting variables from one object type to another and performing mathematical operations, respectively.
The utils class contains a method called CreateRandomString, which is used for generating a unique ID string (randomly generated) for each document.
Code Listing 5-b: docs.dart (The utils Class)
class utils { static final Random _random = Random.secure(); static String CreateRandomString([int length = 32]) { var values = List<int>.generate( length, (i) => _random.nextInt(256)); return base64Url.encode(values); } } |
The generated string returned by the CreateRandomString method is Base64 encoded.
The Doc class is used for representing how documents can be stored as objects, not only during runtime, but also in Firebase. Let’s explore its details.
Code Listing 5-c: docs.dart (The Doc Class)
class Doc { List<String> months = [ 'Jan|01', 'Feb|02', 'Mar|03', 'Apr|04', 'May|05', 'Jun|06', 'Jul|07', 'Aug|08', 'Sep|09', 'Oct|10', 'Nov|11', 'Dec|12' ];
String ID; String Document; String Year; String Month; String Day; String Expires; String Status; String Badge; bool Edit; bool Alert12; bool Alert6; bool Alert3; bool Alert1; int Days; Doc({ this.ID, this.Document, this.Year, this.Month, this.Day, this.Expires, this.Status, this.Badge, this.Edit = true, this.Alert12, this.Alert6, this.Alert3, this.Alert1, this.Days }); // More code to follow } |
We start by looking at the months list, which is used as a placeholder that contains the names of each month, along with their corresponding month number.
List<String> months = ['Jan|01', 'Feb|02', 'Mar|03', 'Apr|04', 'May|05', 'Jun|06', 'Jul|07', 'Aug|08', 'Sep|09', 'Oct|10', 'Nov|11', 'Dec|12'];
This is used within the determineStatus method to calculate how many days remain until a document expires.
Following that, we find the properties that our Doc class uses declared as follows.
String ID;
…
int Days;
Finally, we have the Doc class constructor, which is used to initialize the properties declared.
Doc({this.ID, this.Document, this.Year, this.Month, this.Day, this.Expires,
this.Status, this.Badge, this.Edit = true, this.Alert12, this.Alert6,
this.Alert3, this.Alert1, this.Days});
Notice that by default, the Edit property is set to true within the constructor’s initialization. This means that when we create an instance of the Doc class, we are indicating our intention of using it to edit an existing document or create a new one.
One of the application’s coolest features—which is visible on the app’s main screen (de-base component)—is that, for each document, there is a badge that indicates how many days remain before the document expires.

Figure 5-a: Days Remaining Badge (Right-Hand)
The creation of this badge and calculation of the number of days remaining is an integral part of every document, which is why this functionality is included within the model, and specifically within the Doc class.
Let’s have a look at how this calculation is done by exploring the following two methods.
Code Listing 5-d: docs.dart (Days Remaining Methods)
String getMonthNum(String m) { var n = ''; for (var i = 0; i < months.length; i++) { if (months[i].contains(m)) { var p = months[i].split('|'); n = p[1]; break; } } return n; } List determineStatus(String y, String m, String d) { var r = ''; var badge = ''; var exp = DateTime(int.parse(y), int.parse(getMonthNum(m)), int.parse(d)); var diffDays = exp.difference(DateTime.now()).inDays; if (diffDays <= 0) { r = 'Expired'; r = r.replaceAll('-', ''); badge = 'badge badge-dark ml-2'; } else if (diffDays > 0 && diffDays <= 30) { r = diffDays.toString() + ' day(s)'; badge = 'badge badge-danger ml-2'; } else if (diffDays > 30 && diffDays <= 60) { r = diffDays.toString() + ' day(s)'; badge = 'badge badge-warning ml-2'; } else if (diffDays > 60 && diffDays <= 90) { r = diffDays.toString() + ' day(s)'; badge = 'badge badge-secondary ml-2'; } else if (diffDays > 90 && diffDays <= 120) { r = diffDays.toString() + ' day(s)'; badge = 'badge badge-info ml-2'; } else if (diffDays > 120 && diffDays <= 240) { r = diffDays.toString() + ' day(s)'; badge = 'badge badge-primary ml-2'; } else if (diffDays > 240 && diffDays <= 365) { r = diffDays.toString() + ' day(s)'; badge = 'badge badge-default ml-2'; } else if (diffDays > 365) { r = diffDays.toString() + ' day(s)'; badge = 'badge badge-success ml-2'; } return [r, diffDays, badge]; } |
From the preceding code, we can see that the main method used for calculating the number of days remaining is determineStatus. This method receives the expiration year (y), month (m), and day (d) of the current document as parameters.
These values are then converted into the DateTime representation of the document’s expiration date by the following instruction, for which the getMonthNum method is also used.
var exp = DateTime(int.parse(y), int.parse(getMonthNum(m)), int.parse(d));
The number of days (diffDays) remaining is calculated by the difference between the document’s expiration date (exp) and the current date and time.
var diffDays = exp.difference(DateTime.now()).inDays;
Depending on the number of days remaining (diffDays), the correct badge value (badge) and response (r) are assigned. The badge value represents the CSS class and color scheme, which vary depending on the number of days left.
The determineStatus method returns the badge, text response (r), and the number of days remaining (diffDays). As you have seen, calculating the number of days is not difficult.
As you might recall, in the previous chapter we explored what happens when saving a document (within the de-form component)—specifically on the saveItem method—where we invoke the update method of a Doc instance using the Dart cascade notation.
docs[index]..update(cDoc);
Let’s dive into the details of what the update method does.
Code Listing 5-e: docs.dart (update Method)
void update(Doc doc) { if (doc.ID == '-1') { ID = utils.CreateRandomString(); } Document = doc.Document; Year = doc.Year; Month = doc.Month; Day = doc.Day; Expires = doc.Day + '-' + doc.Month + '-' + doc.Year; var st = determineStatus(Year, Month, Day); Status = st[0]; Badge = st[2];
Alert12 = doc.Alert12; Alert6 = doc.Alert6; Alert3 = doc.Alert3; Alert1 = doc.Alert1; Days = st[1]; } |
The update method receives a Doc object (doc)—the current document being edited or added.
If the ID property has a value of '-1' (which indicates a new document is being added), then the CreateRandomString method is called to assign a unique ID value to the document (doc).
The rest of the update method assigns to the instance properties the respective values of the doc object properties, which we can see as follows.
Document = doc.Document;
Year = doc.Year;
Month = doc.Month;
Day = doc.Day;
Alert12 = doc.Alert12;
Alert6 = doc.Alert6;
Alert3 = doc.Alert3;
Alert1 = doc.Alert1;
The values of the Status, Badge, and Days properties are assigned from the values returned by the determineStatus method, which can be seen as follows.
var st = determineStatus(Year, Month, Day);
Status = st[0];
Badge = st[2];
Days = st[1];
One of the most important characteristics of the application is its ability to sort documents by the number of expiration days. This means the documents that have expired (or with the lowest number of days remaining) are shown first, and documents with the highest number of days before expiration are displayed last.
To achieve this, we rely on the sorter and sortDocs methods. Let’s explore both.
Code Listing 5-f: docs.dart (Sorting Methods)
static Function(Doc, Doc) sorter(int sortOrder, String property) { int handleSortOrder(int sortOrder, int sort) { if (sortOrder == 1) { // a is before b if (sort == -1) { return -1; } else if (sort > 0) { // a is after b return 1; } else { // a is the same as b return 0; } } } return (Doc a, Doc b) { if (a != null && b != null) { switch (property) { case 'Days': var sort = a.Days.compareTo(b.Days); return handleSortOrder(sortOrder, sort); default: break; } } }; } // sortOrder = 1 ascending | 0 descending static void sortDocs(List<Doc> docs, {int sortOrder = 1, String property = 'Days'}) { if (docs.isNotEmpty) { switch (property) { case 'Days': docs.sort(sorter(sortOrder, property)); break; default: print('Unrecognized property $property'); } } } |
The sortDocs method is responsible for sorting the list of documents using the Days property. The sortDocs method invokes the list’s (docs) sort method, to which the sorter method is passed, the method that does the actual sorting.
The sorter method returns a function (Function(Doc, Doc)) that iteratively invokes the handleSortOrder function—also contained within the sorter method—that indicates which of the two elements of the list being compared has precedence (goes first). The process repeats itself for every document on the list of documents.
The function being returned by the sorter method, (Doc a, Doc b), performs the comparison of each pair of elements within the list (a and b), by checking the value of the Days property of each element. This is what a.Days.compareTo(b.Days) does.
As you can see, the comparison for the sorting mechanism logic is very simple and straightforward to understand. The only tricky part is how the code for the sorter method is structured, as it requires the logic to be split into an internal handleSortOrder function and another part that returns the (Doc a, Doc b) function, which gets passed to the list’s sort method.
docs.sort(sorter(sortOrder, property));
To be able to read Doc objects from Firebase, and display the list of documents within the application, we need to be able to read each document stored in Firebase as JSON.
To achieve that, we have to convert the document from JSON to its Doc object equivalent, which we can do with fromMap. Let’s have a look.
Code Listing 5-g: docs.dart (fromMap Method)
Doc.fromMap(Map mp) : this ( ID: mp['ID'].toString(), Document: mp['Document'].toString(), Year: mp['Year'].toString(), Month: mp['Month'].toString(), Day: mp['Day'].toString(), Expires: mp['Expires'].toString(), Status: mp['Status'].toString(), Badge: mp['Badge'].toString(), Edit: mp['Edit']. toString(). toLowerCase() == 'true' ?? false, Alert12: mp['Alert12']. toString(). toLowerCase() == 'true' ?? false, Alert6: mp['Alert6']. toString(). toLowerCase() == 'true' ?? false, Alert3: mp['Alert3']. toString(). toLowerCase() == 'true' ?? false, Alert1: mp['Alert1']. toString(). toLowerCase() == 'true' ?? false, Days: int.parse( mp['Days'].toString()) ); |
What is going on here? The fromMap method takes a Map object (mp) as a parameter—which in Dart represents a key-value pair.
The Doc constructor is invoked, for which the values contained within the mp object are assigned to their respective properties within the Doc instance.
Object values stored in Firebase’s real-time database, when assigned to their corresponding properties within the Doc instance, must be converted to the right data type. This is why the Edit, Alert1, Alert3, Alert6, and Alert12 properties are all converted within the fromMap method to Boolean.
On the other hand, the Days property, which indicates the number of days remaining before expiration, is converted to an integer.
So, when our application retrieves data from Firebase, it uses the fromMap method to convert the document from JSON to its Doc object equivalent. But what happens when we need to take document data stored as Doc objects and write that to Firebase? That’s when we need to use the asMap method. Let’s have a look.
Code Listing 5-h: docs.dart (asMap Method)
Map asMap() => { 'ID': ID, 'Document': Document, 'Year': Year, 'Month': Month, 'Day': Day, 'Expires': Expires, 'Status': Status, 'Badge': Badge, 'Edit': Edit, 'Alert12': Alert12, 'Alert6': Alert6, 'Alert3': Alert3, 'Alert1': Alert1, 'Days': Days }; |
As we can see, the asMap method (written as a lambda expression) creates a JSON object by returning a Map containing each of the values of the Doc object properties. Each key (property name) is assigned its corresponding value.
Awesome—we now have explored how our application’s model works, which is essential to being able to read and write data into Firebase.
Next, we are going to see how to set up and work with Firebase, and then create a service provider that is responsible for handling all the communication from the application to the real-time database.
Once we have done that, we’ll have our AngularDart application ready. We’re close to completing the project—which is exciting!