Skip to content

Dhannesh/app_persist

Repository files navigation

Persisting Data on the Local Device

When building mobile applications, there is often the need to store data locally on the mobile device, either across the application’s sessions or to store user preferences. Based on the size and complexity of the data, there are several choices to store data locally.

Local Persistence in Flutter

img.png Let's say you need to store data locally in Flutter, that is, you need to persist data on your mobile device. What are the options you have? There are a number of different options. We'll discuss just the three most common. The first is you can store data on the file system of your mobile device. This of course involves reading from and writing to files. Or you can store data locally using SharedPreferences. This allows you to store data in the form of key-value pairs typically used for user's preference data. Yet another option is you store data in an embedded database such as SQLite. This allows you to query your data using SQL queries, manipulate your data and everything that you'd expect from a database. When would you choose to use the file system to store data?

The file system is a great option if you have unstructured data, maybe data in the form of long form text and you want to preserve this data across app launches. The file system is also a great option if you want to download and store data that you've accessed from the Internet and you want to be able to access this data offline later on sometime. Another reason to potentially use files to store data is that you want to be able to share the file with other users using some kind of sharing services, maybe e-mail or some kind of messenger service. With Flutter, storing files on your local device is very straightforward. Flutter apps can use libraries to access, read, and write data stored on your device's disk. Your mobile device has a file system, and there are certain locations in that file system that your app can write to.

You'll use the path_provider plugin to find correct local path on disk where you want to write your files. And then you'll use the dart:io library to read data from and write data to the file. The path_provider package is an extremely useful dart library that provides a platform-agnostic way to access locations on the device's file system. With the path_provider, you use the same set of APIs whether you're running your Flutter app on Android, iOS, or some other mobile device. Now, there are different local paths that are accessible by Flutter. There is a temporary directory on your device that is a cache that is available to the system as a whole. The system can clear this at any point-in-time. This temporary directory is not entirely in your app's control, so you may want to think twice before you store your files there. Instead, you may want to use the documents directory.

The documents directory allows the app to store files where only the app can access those files. So your mobile operating system cannot delete files in this documents directory, unless you delete your app. The system clears this data only when the app is deleted. The app controls this data until then. We've discussed persisting data in files on a local device. Next, let's discuss SharedPreferences to persist data locally. First, we'll discuss when you would choose to use SharedPreferences to store data. If you have collections of data where your data is rather lightweight and can be represented using key-value pairs, SharedPreferences is the right choice. As the name implies, this is often used to store user preferences such as font size or theme. This is very useful for small amounts of data, data that is local to the app, data that need not be shared with other people or applications. When you use SharedPreferences to store data, you use the SharedPreferences plugin. This plugin wraps the NSUserDefaults on iOS and the SharedPreferences on Android. The data that you store using SharedPreferences can't be very complex. It supports only primitive types such as int, double, boolean, string, and a list of strings. Data is stored in the form of key-value pairs. Keys are always strings and values can be these other primitive types. Key-value pairs are updated synchronously in memory first and then asynchronously written out to disk. Reading a value using a particular key is always synchronous.

Now let's move on to discussing the third technique that you can use to persist data using SQLite. SQLite is a database engine written in the C programming language. SQLite is actually an embedded database. Software developers use the SQLite library, and that database is embedded into the application. If you want to persist large amounts of data on your local device, you need a way to store and query this data efficiently. You need an interface that supports SQL queries, allowing you to access and manipulate data. That's when SQLite is really useful. This also means that the data that you store in SQLite can be more complex. You can have entities with multiple attributes. You also get faster performance from your queries for inserts, deletes, and updates when you use SQLite. When you use SQLite from within Flutter, you'll need to use the SQFlite package. This is what gives you the classes and functions that you need to interact with the SQLite database. In addition, you'll need to use the path package, which gives you functions to define the location for where you want the database to be stored on disk.

Reading Text Data Using the Flutter Services Library

In this demo, we'll see a number of different techniques that we can use in Flutter to read and write files from the local device. In this first iteration of our application, I'm going to read a file that is present as a part of the asset bundle of our app. So, we'll see how we can read a file that is bundled in along with images and other assets within our application. I have a Flutter project that's already set up and open within Android Studio. I'm going to create a New Directory, which is going to hold the files that will be included as a part of the assets of this app. These will be within the files subfolder. So, within this file subfolder, I'm going to right-click and create a New File, and this file will be named about.txt. This is the file that we'll read from within our app. In order for this file to be included as a part of the assets of this mobile application that we are about to build, I need to make an entry in the pubspec.yaml file. Open up pubspec.yaml and somewhere in the middle, Within the flutter section, go in and add a section for assets and point that to the files subfolder. This will ensure that all of the contents of the files subfolder will be included as a part of the asset bundles that make up our mobile app. The AboutPage is a StatefulWidget that's defined right here in this file. This _data member variable contains the contents of the about.txt file when we load in the contents after having read the file.

The function loadData is an async function within which we actually read in the contents of about.txt and for that I use the rootBundle. This is an async function which is why I need to await the results of this function and the results will be stored in loadedData. I then call setState and set the contents of the about.txt file to my _data variable within setState. Let's take a look at the UI at this AboutPage sets up. Within the build function, This Card has a purple background color And within this Card, I display a Text widget. Now I first check to see whether the _data variable is equal to null. If _data is equal to null, we haven't loaded in the contents of the file yet, and so the Text widget defined displays a large question mark. If _data is not equal to null, that is the file contents have been loaded, we'll display the Text widget. That simply displays the data that we've loaded in from the about.txt file within the Card widget.

Reading and Parsing Contents of a CSV File

In this demo, we'll see how you can read in and parse the contents of a CSV file. We'll then display the contents of the CSV file in a nice tabular format using the data table widget in Flutter. In order to be able to parse the contents of the CSV file, we need an additional library (CSV). You can see that there are four columns of information in this orders.csv file, Date, Status, Category, and Amount. Every entry in this file you can say refers to an order. Now, this file happens to be in the files subfolder which will be included in the asset bundle included with my app.

The csvData can hold null, it's a nullable type. And it contains a List of List of any type, so a List of List of dynamic. The outer list in this type refers to the rows or the records in our csv file. The inner list refers to the individual fields within the record, and the individual fields can be of any type. They can be string, integers, anything. Now, data is loaded from my orders.csv file and parsed as csv in this process csv method defined in orders_page. I invoke DefaultAssetBundle of the current context and invoke the loadString method within it. And I point to the files/orders.csv file. Now, reading a file is always an asynchronous operation. I await the result of this operation, and I store the result in the result variable. The result variable holds the csv contents in the string format. I instantiate the CsvToListConverter and invoke the convert function and pass in the result as an input. And I indicate that the end of line in this csv format is the \n or the newline character. This will parse the csv originally present as a string to a List of List of dynamic types.

Now let's take a look at the UI that's fairly simple. We'll display the csv data in a tabular format, and I'm going to embed the table within a SingleChildScrollView. You can see that the scrollDirection is horizontal, so we kind of overflow the screen so we'll be able to move around horizontally. If the csvData variable is equal to null, then we simply display an empty Container. There is no table to view because we haven't loaded in the file yet. If the csvData variable is not null, we'll display the csv contents in a tabular format and the right widget for that is the DataTable. Now, the DataTable has two properties that I have set here, the columns property and the rows property. The columns property basically defines the headers for this tabular format. I access the first row of the csvData and I map the contents of the first row so that every column header is displayed using a DataColumn widget and the label of the DataColumn is simply the name of the column that is displayed using a Text widget. The rows property is basically all of the rows other than the first header row. I use a List.generate function to generate the DataRow widgets that represent the individual rows. The length of this list is equal to the length of the csvData minus 1.

The app is already running. Let's click on the floatingActionButton with the table icon in order to look at our file contents as a data table. So, you can see this nice tabular format which contains our csv data. You can scroll horizontally in order to view the entire table.

Accessing Files in the App Documents Directory

So far, we've seen how to read in the contents of files that are part of our application's asset bundle. In this demo, we'll see how we can read and write files on our mobile device's local file system, and we'll do this using the path_provider library. So, let's head over to pubspec.yaml and install the latest version of path_provider as a dependency for our app.

The utils folder lives under the lib folder and this is what I use to store the various helper utilities that I'll create. This will contain utility classes. The utility class for this demo is file_util.dart.

The main.dart uses home_page.dart to build the UI. in home_page.dart contents a nullable variable of type String. This is what we'll use to store the content of the file when we read from the local file system. I also have a text field which will be controlled using the TextEditingController. The TextField that we'll use to update the contents of a file present on the local file system. Observe the onPressed handler for the ElevatedButton where we invoke the helper function FileUtil.writeNote in order to write the text contents that we've typed in to a particular file, and then we clear the _textController. This is the Text widget that we display the content of the file as we read from the file system.

Now let's take a look at the utility functions in file_util.dart. That is the most important part here. First, let's take a look at the import statements. I have an import for path_provider. dart. This is a cross-platform path manipulation library for dart which provides common operations for manipulating paths, joining paths together, splitting, normalizing, and so on. There is a static method called _getShoppingNotesDirPath. This will basically give us access to the directory on our local device where we plan to store our notes. I access the ApplicationsDocumentsDirectory. This is a directory for the app to store files that only the app can access. The system itself will not touch this directory as long as you have the app installed. This directory will be cleared only when the app itself is deleted.

It's best practice to use the application documents directory to store files that you want to store locally on your device, so the external system will not tamper with those files as long as you have the app on your device. now that I've got the appDocsDir.path, I'm now going to set up a subfolder within the appDocsDir, that is the shoppingNotesDirPath. And I create the subfolder by using the join function and join the path for the appDocsDir with shopping notes. I instantiate a Directory object for the shoppingNotesDir. I check to see whether the directory already exists. I do this check in a synchronous fashion using existsSync. If the directory does not exist, I create the directory in a synchronous fashion using shoppingNotesDir.createSync. Once this subfolder has been created successfully, I simply return the shoppingNotesDirPath. This is wrapped in a future that holds a string.

In order to read the note within our shoppingNotesDir, I first need to get the path to my shoppingNotesDir, and I do that using _getShoppingNotesDirtPath using the await keyword because this is an async function. Once I have the path I join the shoppingNotesDirPath with the filename notes.txt. So, I always read from and write to the notes.txt file. I instantiate a file object using the shoppingNotePath, and then I invoke notesFile.readAsString that asynchronously reads the contents of the file and returns the contents in the string format. Click on Read from the file and you'll see the new contents are now present in that file. The old contents have been overwritten.

img_1.png

Listing Files Present on the Local Device

We'll put together what we've studied so far and create a nice little application to manage all our shopping notes. So we won't be reading and writing to a single notes.txt file. Instead, we'll have multiple notes that we'll be able to manage within our app. We'll be able to delete notes, edit the contents of notes, and so on. file_util.dart contains listNotes that will return all of the notesFiles that we currently have under our application documents directory, shopping notes subdirectory. Now let's head over to main.dart, and I'm going to update the code here. The main.dart file creates and runs a MaterialApp that displays the shoppingNotes widget. We have a member variable called notesFuture, and this is the Future object that contains the List of notes that we have in our shopping notes directory on the local file system. I invoke FileUtil.listNotes and initialize the notesFuture. This is what we'll use to set up a grid view representation of all of the notes that we have saved. I instantiate a FutureBuilder that will allow me to create widget tree using a snapshot of data that I retrieve from a future object and the future object is the notesFuture assigned to the future property.

The builder property contains the callback that is invoked to build up the widget tree once the data in the future is available. If the snapshot hasData, The number of items in the grid view is equal to the length of the snapshot data. The grid view items are essentially the files containing our notes. Now, every file is represented using an InkWell widget. Now, every InkWell has an onTap handler. I haven't really defined what the onTap handler does. We'll get to that a little bit later as we build up this application step by step.

Creating, Editing, and Deleting Files

In our app so far we can see all of the notes that we have available, but we can't really view the contents of a note by clicking on it. We can't even add a new note by clicking on the floatingActionButton. So, you can see the functionality of this app is a little bit limited.

Let's fix this in this video. We'll add the ability to view the contents of a note and also edit and update a note and create new notes. In the file_util helper class, I've updated the readNote and writeNote functions. we create a new stateful widget Note in note.dart, Now when we instantiate the Note widget, we need to pass the file that corresponds to this Note as an input argument that's stored in the noteFile. Now if this is a brand new Note, noteFile will be null.

Now within this State, I have two text controllers, one text controller which manages the name of the note this is something that you can edit, and the second _contentTextController that is used to manage the notes contents, that you type out. It's either a new note or you're editing the existing contents of a note. We'll initialize these controllers within the initState method. I check to see whether this is a new note or a note that already exists. If nodeFile is not equal to null, then this is an already existing note that we want to edit. I access the name of the note that is the fileName using the basename function to extract the name of the note from the full path to the noteFile. I invoke FileUtil.readNote. This will allow us to read in the contents of the note and display these contents on the text field controlled by the _contentTextController. Now if this happens to be a brand new note that we are creating, it's not an already existing note, set the _nameTextController.text property to some_note.txt. That is the default name of the note and the content is just the empty string.

Notice the appBar has a title that is the TextField. This is the TextField that displays the name of the note. Now since this is a TextField that is editable, this means that you can edit the text here to edit the name of the note. And then within an Expanded widget, I have the TextField that displays the content of the note, Notice I've set the decoration of the TextField to null, so that we just have a blank TextField with the contents of the note.

This is the floatingActionButton which when pressed will save the contents of your note. we call FileUtil.writeNote and I pass in the name of the note and the contents of the note. Navigator.pop will then basically pop us back to the previous grid view so we are no longer in the note editing page. Now we need to wire up access to this Note widget from the shopping_notes grid view.

Now, each time we click on a particular note on shopping_notes, we'll navigate to the Note widget, we want to be able to access the note in order to be able to edit its contents. Let's update the onTap handler of the InkWell. Now, after editing the note, when we navigate back to the shopping notes page, I want a few actions to be performed. When we come back to the shopping notes page, I invoke FileUtil.listNotes once again. This will include the note that was just edited, maybe its name was changed or its contents were changed, or any new note that we added. I also call setState in order to update the display. Let's also scroll further down and wire up the onPressed handler of the floatingActionButton that will allow us to create a new note. If the floatingActionButton for the new note is pressed, we'll navigate to the Note widget.

Now, let's add one last bit of functionality to our app, and that is the ability to delete a note. For that, I'll first head over to file_util.dart and add a new helper function. This is the deleteNote helper function that takes in the name of a note as an input argument. We access the notesFile and then call notesFile.delete. That's all you need to do from the file system perspective to delete a note. Let's add this functionality to our UI. I'm going to do this in shopping_notes.dart where we display a grid view of the notes that we have. I'm going to wrap the Inkwell representing a note with the Dismissible widget. The Dismissible widget will allow us to slide a particular note off the screen and when the note slides out, it will invoke the onDismissed handler.

I've set the key to be a UniqueKey for each widget. I've also wired up the onDismissed handler. When a particular note is dismissed, that is swiped away from the view, this onDismissed handler will be invoked and within that, we simply invoke FileUtil.deleteNote and pass in the name of the note. We get the name by invoking the basename function on the notes path. And that's it. That's all the wiring up you need to do in order to be able to delete a note.

About

Persisting Data on the Local Device

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published