13
Saving Data with Local Persistence

WHAT YOU WILL LEARN IN THIS CHAPTER

  • How to persist saving and reading data locally
  • How to structure data by using the JSON file format
  • How to create model classes to handle JSON serialization
  • How to access local iOS and Android filesystem locations using the path provider package
  • How to format dates by using the internationalization package
  • How to use the Future class with the showDatePicker to present a calendar to choose dates
  • How to use the Future class to save, read, and parse JSON files
  • How to use the ListView.separated constructor to section records with a Divider
  • How to use List().sort to sort journal entries by date
  • How to use textInputAction to customize keyboard actions
  • How to use FocusNode and FocusScope with the keyboard onSubmitted to move the cursor to the next entry's TextField
  • How to pass and receive data in a class by using the Navigator

In this chapter, you'll learn how to persist data—that is, save data on the device's local storage directory—across app launches by using the JSON file format and saving the file to the local iOS and Android filesystem. JavaScript Object Notation (JSON) is a common open‐standard and language‐independent file data format with the benefit of being human‐readable text. Persisting data is a two‐step process; first you use the File class to save and read data, and second, you parse the data from and to a JSON format. You'll create a class to handle saving and reading the data file that uses the File class. You'll also create a class to parse the full list of data by using json.encode and json.decode and a class to extract each record. And you'll create another class to handle passing an action and an individual journal entry between pages.

You'll build a journal app that saves and reads JSON data to the local iOS NSDocumentDirectory and Android AppData filesystem. The app uses a ListView to display a list of journal entries sorted by date, and you'll create a data entry screen to enter a date, mood, and note.

UNDERSTANDING THE JSON FORMAT

The JSON format is text‐based and is independent of programming languages, meaning any of them can use it. It's a great way to exchange data between different programs because it is human‐readable text. JSON uses the key/value pair, and the key is enclosed in quotation marks followed by a colon and then the value like "id":"100". You use a comma (,) to separate multiple key/value pairs. Table 13.1 shows some examples.

TABLE 13.1: Key/Value Pairs

KEY COLON VALUE
"id" 
:
"100" 
"quantity" 
: 3
"in_stock" 
: true

The types of values you can use are Object, Array, String, Boolean, and Number. Objects are declared by curly ({}) brackets, and inside you use the key/value pair and arrays. You declare arrays by using the square ([]) brackets, and inside you use the key/value or just the value. Table 13.2 shows some examples.

TABLE 13.2: Objects and Arrays

TYPE SAMPLE
Object
{
 "id": "100",
 "name": "Vacation"
} 
Array with values only
["Family", "Friends", "Fun"] 
Array with key/value
[
 {
  "id": "100",
  "name": "Vacation"
 },
 {
  "id": "102",
  "name": "Birthday"
 }
] 
Object with array
{
 "id": "100",
 "name": "Vacation",
 "tags": ["Family", "Friends", 
"Fun"]
} 
Multiple objects with arrays

{
 "journals":[
  {
    "id":"4710827",
    "mood":"Happy"
  },
  {
    "id":"427836",
    "mood":"Surprised"
  },
 ],
 "tags":[
  {
   "id": "100",
   "name": "Family"
  },
  {
   "id": "102",
   "name": "Friends"
  }
 ]
} 

The following is an example of the JSON file that you'll create for the journal application. The JSON file is used to save and read the journal data from the device local storage area, resulting in data persistence over app launches. You have the opening and closing curly brackets declaring an object. Inside the object, the journal's key contains an array of objects separated by a comma. Each object inside the array is a journal entry with key/value pairs declaring the id, date, mood, and note. The id key value is used to uniquely identify each journal entry and isn't displayed in the UI. How the value is obtained depends on the project requirement; for example, you can use sequential numbers or calculate a unique value by using characters and numbers (universally unique identifier [UUID]).

{
 "journals":[
  {
    "id":"470827",
    "date":"2019-01-13 00:27:10.167177",
    "mood":"Happy",
    "note":"Cannot wait for family night."
  },
  {
    "id":"427836",
    "date":"2019-01-12 19:54:18.786155",
    "mood":"Happy",
    "note":"Great day watching our favorite shows."
  },
 ],
}

USING DATABASE CLASSES TO WRITE, READ, AND SERIALIZE JSON

To create reusable code to handle the database routines such as writing, reading, and serializing (encoding and decoding) data, you'll place the logic in classes. You'll create four classes to handle local persistence, with each class responsible for specific tasks.

  • The DatabaseFileRoutines class uses the File class to retrieve the device local document directory and save and read the data file.
  • The Database class is responsible for encoding and decoding the JSON file and mapping it to a List.
  • The Journal class maps each journal entry from and to JSON.
  • The JournalEdit class is used to pass an action (save or cancel) and a journal entry between pages.

The DatabaseFileRoutines class requires you to import the dart:io library to use the File class responsible for saving and reading files. It also requires you to import the path_provider package to retrieve the local path to the document directory. The Database class requires you to import the dart:convert library to decode and encode JSON objects.

The first task in local persistence is to retrieve the directory path where the data file is located on the device. Local data is usually stored in the application documents directory; for iOS, the folder is called NSDocumentDirectory, and for Android it's AppData. To get access to these folders, you use the path_provider package (Flutter plugin). You'll be calling the getApplicationDocumentsDirectory() method, which returns the directory giving you access to the path variable.

Future<String> get _localPath async {
 final directory = await getApplicationDocumentsDirectory();

 return directory.path;
}

Once you retrieve the path, you append the data filename by using the File class to create a File object. You import the dart:io library to use the File class, giving you a reference to the file location.

final path = await _localPath;
Final file = File('$path/local_persistence.json');

Once you have the File object, you use the writeAsString() method to save the file by passing the data as a String argument. To read the file, you use the readAsString() method without any arguments. Note that the file variable contains the documents folder's path and the data filename.

// Write the file
file.writeAsString('$json');
// Read the file
String contents = await file.readAsString();

As you learned in the “Understanding the JSON Format” section, you use a JSON file to save and read data from the device local storage. The JSON file data is stored as plain text (strings). To save data to the JSON file, you use serialization to convert an object to a string. To read data from the JSON file, you use deserialization to convert a string to an object. You use the json.encode() method to serialize and the json.decode() method to deserialize the data. Note that both the json.encode() and json.decode() methods are part of the JsonCodec class from the dart:convert library.

To serialize and deserialize JSON files, you import the dart:convert library. After calling the readAsString() method to read data from the stored file, you need to parse the string and return the JSON object by using the json.decode() or jsonDecode() function. Note that the jsonDecode() function is shorthand for json.decode().

// String to JSON object
final dataFromJson = json.decode(str);
// Or
final dataFromJson = jsonDecode(str);

To convert values to a JSON string, you use the json.encode() or jsonEncode() function. Note that jsonEncode() is shorthand for json.encode(). It's a personal preference deciding which approach to use; in the exercises, you'll be using json.decode() and json.encode().

// Values to JSON string
json.encode(dataToJson);
// Or
jsonEncode(dataToJson);

FORMATTING DATES

To format dates, you use the intl package (Flutter plugin) providing internationalization and localization. The full list of available date formats is available on the intl package page site at https://pub.dev/packages/intl. For our purposes, you'll use the DateFormat class to help you N format and parse dates. You'll use the DateFormat named constructors to format the date according to the specification. To format a date like Jan 13, 2019, you use the DateFormat.yMMD() constructor, and then you pass the date to the format argument, which expects a DateTime. If you pass the date as a String, you use DateTime.parse() to convert it to a DateTime format.

// Formatting date examples
print(DateFormat.d().format(DateTime.parse('2019-01-13')));
print(DateFormat.E().format(DateTime.parse('2019-01-13')));
print(DateFormat.y().format(DateTime.parse('2019-01-13')));
print(DateFormat.yMEd().format(DateTime.parse('2019-01-13')));
print(DateFormat.yMMMEd().format(DateTime.parse('2019-01-13')));
print(DateFormat.yMMMMEEEEd().format(DateTime.parse('2019-01-13')));

I/flutter (19337): 13
I/flutter (19337): Sun
I/flutter (19337): 2019
I/flutter (19337): Sun, 1/13/2019
I/flutter (19337): Sun, Jan 13, 2019
I/flutter (19337): Sunday, January 13, 2019

To build additional custom date formatting, you can chain and use the add_*() methods (substitute the * character with the format characters needed) to append and compound multiple formats. The following sample code shows how to customize the date format:

// Formatting date examples with the add_* methods
print(DateFormat.yMEd().add_Hm().format(DateTime.parse('2019-01-13 10:30:15')));
print(DateFormat.yMd().add_EEEE().add_Hms().format(DateTime.parse('2019-01-13 10:30:15')));

I/flutter (19337): Sun, 1/13/2019 10:30
I/flutter (19337): 1/13/2019 Sunday 10:30:15

SORTING A LIST OF DATES

You learned how to format dates easily, but how would you sort dates? The journal app that you'll create requires you to show a list of entries, and it would be great to be able to display the list sorted by date. In particular, you want to sort dates to show the newest first and oldest last, which is known as DESC (descending) order. Our journal entries are displayed from a List, and to sort them, you call the List().sort() method.

The List is sorted by the order specified by the function, and the function acts as a Comparator, comparing two values and assessing whether they are the same or whether one is larger than the other—such as the dates 2019‐01‐20 and 2019‐01‐22 in Table 13.3. The Comparator function returns an integer as negative, zero, or positive. If the comparison—for example, 2019‐01‐20 > 2019‐01‐22—is true, it returns 1, and if it's false, it returns ‐1. Otherwise (when the values are equal), it returns 0.

TABLE 13.3: Sorting Dates

COMPARE TRUE SAME FALSE
date2.compareTo(date1) 1 0 ‐1
2019‐01‐20 > 2019‐01‐22 ‐1
2019‐01‐20 < 2019‐01‐22 1
2019‐01‐22 = 2019‐01‐22 0

Let's take a look by running the following sort with actual DateTime values sorted by DESC date. Note to sort by DESC, you start with the second date, comparing it to the first date like this: comp2.date.compareTo(comp1.date).

_database.journal.sort((comp1, comp2) => comp2.date.compareTo(comp1.date));

// Results from print() to the log
I/flutter (10272): -1 - 2019-01-20 15:47:46.696727 - 2019-01-22 17:02:47.678590
I/flutter (10272): -1 - 2019-01-19 15:58:23.013360 - 2019-01-20 15:47:46.696727
I/flutter (10272): -1 - 2019-01-19 13:04:32.812748 - 2019-01-19 15:58:23.013360
I/flutter (10272): 1 - 2019-01-22 17:21:12.752577 - 2018-01-01 16:43:05.598094
I/flutter (10272): 1 - 2019-01-22 17:21:12.752577 - 2018-12-25 02:40:55.533173
I/flutter (10272): 1 - 2019-01-22 17:21:12.752577 - 2019-01-16 02:40:13.961852

I wanted to show you the longer way to the previous code's sort() to show how the compare result is obtained.

_database.journal.sort((comp1, comp2) {
 int result = comp2.date.compareTo(comp1.date);
 print('$result - ${comp2.date} - ${comp1.date}');
 return result;
});

If you would like to sort the dates by ASC (ascending) order, you can switch the compare statement to start with comp1.date to comp2.date.

_database.journal.sort((comp1, comp2) => comp1.date.compareTo(comp2.date));

RETRIEVING DATA WITH THE FUTUREBUILDER

In mobile applications, it is important not to block the UI while retrieving or processing data. In Chapter 3, “Learning Dart Basics,” you learned how to use a Future to retrieve a possible value that is available sometime in the future. A FutureBuilder widget works with a Future to retrieve the latest data without blocking the UI. The three main properties that you set are initialData, future, and builder.

  • initialData: Initial data to show before the snapshot is retrieved.

    Sample code:

    []

  • future: Calls a Future asynchronous method to retrieve data.

    Sample code:

    _loadJournals()

  • builder: The builder property provides the BuildContext and AsyncSnapshot (data retrieved and connection state). The AsyncSnapshot returns a snapshot of the data, and you can also check for the ConnectionState to get a status on the data retrieval process.

    Sample code:

    (BuildContext context, AsyncSnapshot snapshot)

  • AsyncSnapshot: Provides the most recent data and connection status. Note that the data represented is immutable and read‐only. To check whether data is returned, you use the snapshot.hasData. To check the connection state, you use the snapshot.connectionState to see whether the state is active, waiting, done, or none. You can also check for errors by using the snapshot.hasError property.

    Sample code:

    builder: (BuildContext context, AsyncSnapshot snapshot) {

    return !snapshot.hasData

    ? CircularProgressIndicator()

    : _buildListView(snapshot);

    },

The following is some FutureBuilder() sample code:

FutureBuilder(
 initialData: [],
 future: _loadJournals(),
 builder: (BuildContext context, AsyncSnapshot snapshot) {
  return !snapshot.hasData
    ? Center(child: CircularProgressIndicator())
    : _buildListViewSeparated(snapshot);
 },
),

BUILDING THE JOURNAL APP

You'll be building a journal app with the requirement of persisting data across app starts. The data is stored as JSON objects with the requirements of tracking each journal entry's id, date, mood, and note (Figure 13.1). As you learned in the “Understanding the JSON Format” section, the id key value is unique, and it's used to identify each journal entry. The id key is used behind the scenes to select the journal entry and is not displayed in the UI. The root object is a key/value pair with the key name of 'journal' and the value as an array of objects containing each journal entry.

Screenshot of Journal app.

FIGURE 13.1: Journal app

The app has two separate pages; the main presentation page uses a ListView sorted by DESC date, meaning last entered record first. You utilize the ListTile widget to format how the List of records is displayed. The second page is the journal entry details where you use a date picker to select a date from a calendar and TextField widgets for entering the mood and note data. You'll create a database Dart file with classes to handle the file routines, database JSON parsing, a Journal class to handle individual records, and a JournalEdit class to pass data and actions between pages.

Figure 13.2 shows a high‐level view of the journal app detailing how the database classes are used in the Home and Edit pages.

Schematic of Journal app database classes' relationship to the Home and Edit pages.

FIGURE 13.2: Journal app database classes' relationship to the Home and Edit pages

Adding the Journal Database Classes

You'll create four separate classes to handle the database routines and serialization to manage the journal data. Each class is responsible for handling specific code logic, resulting in code reusability. Collectively the database classes are responsible for writing (saving), reading, encoding, and decoding JSON objects to and from the JSON file.

  • The DatabaseFileRoutines class handles getting the path to the device's local documents directory and saving and reading the database file by using the File class. The File class is used by importing the dart:io library, and to obtain the documents directory path, you import the path_provider package.
  • The Database class handles decoding and encoding the JSON objects and converting them to a List of journal entries. You call databaseFromJson to read and parse from JSON objects. You call databaseToJson to save and parse to JSON objects. The Database class returns the journal variable consisting of a List of Journal classes, List<Journal>. The dart:convert library is used to decode and encode JSON objects.
  • The Journal class handles decoding and encoding the JSON objects for each journal entry. The Journal class contains the id, date, mood, and note journal entry fields stored as Strings.
  • The JournalEdit class handles the passing of individual journal entries between pages. The JournalEdit class contains the action and journal variables. The action variable is used to track whether the Save or Cancel button is pressed. The journal variable contains the individual journal entry as a Journal class containing the id, date, mood, and note variables.

Adding the Journal Entry Page

The entry page is responsible for adding and editing a journal entry. You might ask, how does it know when to add or edit a current entry? You created the JournalEdit class in the database.dart file for this reason—to allow you to reuse the same page for multiple purposes. The entry page extends a StatefulWidget with the constructor (Table 13.4) having the three arguments add, index, and journalEdit. Note that the index argument is used to track the selected journal entry location in the journal database list from the Home page. However, if a new journal entry is created, it does not exist in the list yet, so a value of ‐1 is passed instead. Any index numbers zero and up would mean the journal entry already exists in the list.

TABLE 13.4: EditEntry Class Constructor Arguments

VARIABLE DESCRIPTION AND VALUE
final bool add If the add variable value is true, it means you are adding a new journal. If the value is false, you are editing a journal entry.
final int index If the index variable value is ‐1, it means you are adding a new journal entry. If the value is 0 or greater, you are editing a journal entry, and you need to track the index position in the List<Journal>.
final JournalEdit journalEdit
String action
Journal journal
The JournalEdit class passes two values. The action value is either 'Save' or 'Cancel'. The journal variable passes the entire Journal class, consisting of the id, date, mood, and note values.

The entry page has Cancel and Save buttons that call an action with the onPressed() method (Table 13.5). The onPressed() method sends back to the Home page the JournalEdit class with appropriate values depending on which button is pressed.

TABLE 13.5: Save or Cancel FlatButton

ONPRESSED() RESULT
Cancel The JournalEdit action variable is set to 'Cancel', and the class is passed back to the Home page with Navigator.pop(context, _journalEdit).
The Home page receives the values and does not take any action since the editing was canceled.
Save The JournalEdit action variable is set to 'Save', and the journal variable is set with the current Journal class values, the id, date, mood, and note. If the add value is equal to true, meaning adding a new entry, a new id value is generated. If the add value is equal to false, meaning editing an entry, the current journal id is used.
The Home page receives the values and executes the 'Save' logic with received values.

To make it easy for the user to select a date, you use the built‐in date picker that presents a calendar. To show the calendar, you call the showDatePicker()function (Table 13.6) and pass four arguments: context, initialDate, firstDate, and lastDate (Figure 13.3).

TABLE 13.6: showDatePicker

PROPERTY VALUE
context You pass the BuildContext as the context.
initialDate You pass the journal date that is highlighted and selected in the calendar.
firstDate The oldest date range available to be picked in the calendar from today's date.
lastDate The newest date range available to be picked in the calendar from today's date.
Screnshot of date picker calendar.

FIGURE 13.3: Date picker calendar

Once the date is retrieved, you'll use the DateFormat.yMMMEd() constructor to show it in Sun, Jan 13, 2018 format. If you would like to show a time picker, call the showTimePicker() method and pass the context and initialTime arguments.

In Chapter 6, “Using Common Widgets,” you learned how to use a Form with the TextFormField to create an entry form. Now let's use a different approach without a Form but use the TextField with a TextEditingController. You'll learn how to use the TextField TextInputAction with the FocusNode to customize the keyboard action button to execute a custom action (Figure 13.4). The keyboard action button is located to the right of the spacebar. You'll also learn how to customize the TextField capitalization options by using TextCapitalization that configures how the keyboard capitalizes words, sentences, and characters; the settings are words, sentences, characters, or none (default).

Screenshot of keyboard action button for iOS and Android

FIGURE 13.4: Keyboard action button for iOS and Android

Finishing the Journal Home Page

The Home page is responsible for showing a list of journal entries. In Chapter 9, “Creating Scrolling Lists and Effects,” you learned how to use the ListView.builder, but for this app, you'll learn how to use the ListView.separated constructor. By using the separated constructor, you have the same benefits of the builder constructor because the builders are called only for the children who are visible on the page. You might have noticed I said builders, because you use two of them, the standard itemBuilder for the List of children (journal entries) and the separatorBuilder to show a separator between children. The separatorBuilder is extremely powerful for customizing the separator; it could be an Image, Icon, or custom widget, but for our purposes, you'll use a Divider widget. You'll use the ListTile to format your list of journal entries and customize the leading property with a Column to show the date and day of the week, making it easier to spot individual entries (Figure 13.5).

Screenshot of Journal entry list.

FIGURE 13.5: Journal entry list

To delete journal entries, you'll use a Dismissible, which you learned about in Chapter 11, “Applying Interactivity.” Keep in mind that for the Dismissible to work properly and delete the correct journal entry, you'll set the key property to the journal entry id field by using the Key class, which takes a String value like Key(snapshot.data[index].id).

You'll learn how to use the FutureBuilder widget, which works with a Future to retrieve the latest data without blocking the UI. You learned details in this chapter's “Retrieving Data with the FutureBuilder” section.

You'll use a Future that you learned how to use in Chapters 3 and 12 (“Learning Dart Basics” and “Writing Platform‐Native Code”) to retrieve the journal entries. Retrieving the journal entries requires multiple steps, and to help you manage them, you'll use the database.dart file classes that you created in the “Adding the Journal Database Classes” section of this chapter. The classes that you use are DatabaseFileRoutines, Database, Journal, and JournalEdit.

  1. You call the DatabaseFileRoutines calls to read the JSON file located in the device documents folder.
  2. You call the Database class to parse the JSON to a List format.
  3. You use the List sort function to sort entries by DESC date.
  4. The sorted List is returned to the FutureBuilder, and the ListView displays existing journal entries.

SUMMARY

In this chapter, you learned how to persist data by saving and reading locally to the iOS and Android device filesystem. For the iOS device, you used the NSDocumentDirectory, and for the Android device, you used the AppData directory. The popular JSON file format was used to store the journal entries to a file. You created a journaling mood app that sorts the list of entries by DESC date and allows adding and modifying records.

You learned how to create the database classes for handling local persistence to encode and decode JSON objects and write and read entries to a file. You learned how to create the DatabaseFileRoutines class to obtain the path of the local device documents directory and save and read the database file using the File class. You learned how to create the Database class handling decoding and encoding JSON objects and converting them to a List of journal entries. You learned how to use json.encode to parse values to a JSON string and json.decode to parse the string to a JSON object. The Database class returns a List of Journal classes, List<Journal>. You learned how to create the Journal class to handle decoding and encoding the JSON objects for each journal entry. The Journal class contains the id, date, mood, and note fields stored as String type. You learned how to create the JournalEdit class responsible for passing an action and individual Journal class entries between pages.

You learned how to create a journal entry page that handles both adding and modifying an existing journal entry. You learned how to use the JournalEdit class to receive a journal entry and return it to the Home page with an action and the modified entry. You learned how to call showDatePicker() to present a calendar to select a journal date. You learned to use the DateFormat class with different formatting constructors to display dates like ‘Sun, Jan 13, 2019'. You learned how to use DateTime.parse() to convert a date saved as String to a DateTime instance. You learned how to use the TextField widget with the TextEditingController to access entry values. You learned how to customize the keyboard action button by setting the TextField TextInputAction. You learned how to move the focus between TextField widgets by using the FocusNode and the keyboard action button.

You learned how to create the Home page to show a list of journal entries sorted by DESC date separated by a Divider. You learned how to use the ListView.separated constructor to easily separate each journal entry with a separator. ListView.separated uses two builders, and you learned how to use the itemBuilder for showing the List of journal entries and the separatorBuilder to add a Divider widget between entries. You used the ListTile to format the List of journal entries easily. You learned how to customize the leading property with a Column to show the day and weekday on the leading side of the ListTile. You used the Dismissible widget to make it easy to delete journal entries by swiping left or right on the entry itself (ListTile). You used the Key('id') constructor to set a unique key for each Dismissible widget to make sure the correct journal entry is deleted.

In the next chapter, you'll learn how to set up the Cloud Firestore backend NoSQL database. Cloud Firestore allows you to store, query, and synchronize data across devices without setting up your own servers.

image WHAT YOU LEARNED IN THIS CHAPTER

TOPIC KEY CONCEPTS
Database classes The four database classes collectively manage local persistence by writing, reading, encoding, and decoding JSON objects to and from the JSON file.
DatabaseFileRoutines class This handles the File class to retrieve the device's local document directory and save and read the data file.
Database class This handles decoding and encoding the JSON objects and converting them to a List of journal entries.
Journal class This handles decoding and encoding the JSON objects for each journal entry.
JournalEdit class This handles the passing of individual journal entries between pages and the action taken.
showDatePicker This presents a calendar to select a date.
DateFormat This formats dates by using different formatting constructors.
DateTime.parse() This converts a String into a DateTime instance.
TextField This allows text editing.
TextEditingController This allows access to the value of the associated TextField.
TextInputAction The TextField TextInputAction allows the customization of the keyboard action button.
FocusNode This moves the focus between TextField widgets.
ListView.separated This uses two builders, the itemBuilder and the separatorBuilder.
Dismissible Swipe to dismiss by dragging. Use the onDismissed to call custom actions such as deleting a record.
List().sort Sort a List by using a Comparator.
Navigator This is used to navigate to another page. You can pass and receive data in a class by using the Navigator.
Future You can retrieve possible values available sometime in the future.
FutureBuilder This works with a Future to retrieve the latest data without blocking the UI.
CircularProgressIndicator This is a spinning circular progress indicator showing that an action is running.
path_provider package You can access local iOS and Android filesystem locations.
intl package You can use DateFormat to format dates.
dart:io library You can use the File class.
dart:convert library You can decode and encode JSON objects.
dart:math This is used to call the Random() number generator.
..................Content has been hidden....................

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