Data Persistence on Flutter

See how to persist data to storage in a Flutter app, including to files and to a remote datastore, and use a Repository interface for the persistence. By JB Lorenzo.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

Swapping out the Persistence Layer

If you look into the LocalKeyValuePersistence class, you see it is implementing an abstract class called Repository.

class LocalKeyValuePersistence implements Repository {
  ...
}

This abstract class defines an interface for how you save your data. You can swap out different ways, say to disk, or to the cloud, or even maybe via SQLite. If you look into the file Repository.dart you will see the following:

abstract class Repository {
  void saveString(String userId, String key, String value);

  Future<String> saveImage(String userId, String key, Uint8List image);

  void saveObject(String userId, String key, Map<String, dynamic> object);

  Future<String> getString(String userId, String key);

  Future<Uint8List> getImage(String userId, String key);

  Future<Map<String, dynamic>> getObject(String userId, String key);

  Future<void> removeString(String userId, String key);

  Future<void> removeImage(String userId, String key);

  Future<void> removeObject(String userId, String key);
}

To clarify, you have save, get, and remove methods for three different types: String, Image, and Object. You also notice the presence of userId in the parameters. This allows the persistence layer to have enough information for separating data between users, since there is a login component in the app.

To use the version of Repository that saves via key-value storage, open main.dart and replace the repository parameter in Storage.create with this:

Storage.create(
  repository: LocalKeyValuePersistence(),
)

Try building and running your app. At this time, you see that when you restart the app a second time, you are no longer being asked for your username because the app remembers you now. The app goes right to the Magic Cart screen. Hooray!

Removing Plain Text with Key-Value Store

If you try to logout from the app on the cart screen, then restart the app, you will still see the previous username that you have. In order to clear this on logout, you need to implement removeString in LocalKeyValuePersistence.

@override
Future<void> removeString(String userId, String key) async {
  final prefs = await SharedPreferences.getInstance();
  await prefs.remove(_generateKey(userId, key));
}

This is very similar to previous code: it just calls the remove method on an instance of SharedPreferences using the key you provided.

Now that you added removeString, build and run the app again. Then check after logging out and restarting the app that you are no longer logged in.

Next, the question is how to store the image that you selected so that it persists even after the app restarts?

Saving Image with Key-Value Store

In order to save the image you selected, you need to serialize it. Since you are using a key-value store, one option for serialization is Base64 encoding. This encoding converts your image represented as a byte array into a string.

In order to save, get and remove images with a key-value store, update LocalKeyValuePersistence to use the following methods:

@override
Future<String> saveImage(String userId, String key, Uint8List image) async {
  // 1
  final base64Image = Base64Encoder().convert(image);
  final prefs = await SharedPreferences.getInstance();
  // 2
  await prefs.setString(_generateKey(userId, key), base64Image);
  // 3
  return key;
}

@override
Future<Uint8List> getImage(String userId, String key) async {
  final prefs = await SharedPreferences.getInstance();
  // 4
  final base64Image = prefs.getString(_generateKey(userId, key));
  // 5
  if (base64Image != null) return Base64Decoder().convert(base64Image);
  // 6
  return null;
}

Use the same keystroke as before to add the import for Base64Encoder and Base64Decoder.

You have now handled the common operations for persisting image data. Here’s what you have done:

  1. Convert the image into a Base64 string.
  2. Save the string using the generated key.
  3. Return the key as the identifier used to save the image.
  4. When getting an image, get the Base64 string of the image using key.
  5. If that was not null, convert it back to a byte array.
  6. Return null if there was no image.

Build and run the project and now you should see that your image is restored even when you restart the application. You can also implement remove on your own as an additional exercise, or check the final project in the tutorial materials.

Now that you are saving images, you also want to persist the shopping cart data.

Saving Objects into a Key-Value Store

Before saving objects to a persistence layer, you need to serialize them.

Open lib/Storage.dart and go to _saveCart. You will see the code below.

void _saveCart() async {
  await _repository.saveObject(_user.id, 'cart', _cart.toMap());
}

Notice that when saving the cart object, you call, toMap(). This serializes the cart object into a Map; check out the implementation inside lib/models/Cart.dart.

The map can be saved by serializing it further to a string. This string can then be saved, read and deleted the same way you did in the previous sections. In order to do that, update LocalKeyValuePersistence with the following methods for objects:

@override
void saveObject(String userId, String key, Map<String, dynamic> object) async {
  final prefs = await SharedPreferences.getInstance();
  // 1
  final string = JsonEncoder().convert(object);
  // 2
  await prefs.setString(_generateKey(userId, key), string);
}

@override
Future<Map<String, dynamic>> getObject(String userId, String key) async {
  final prefs = await SharedPreferences.getInstance();
  // 3
  final objectString = prefs.getString(_generateKey(userId, key));
  // 4
  if (objectString != null)
    return JsonDecoder().convert(objectString) as Map<String, dynamic>;
  return null;
}

@override
Future<void> removeObject(String userId, String key) async {
  final prefs = await SharedPreferences.getInstance();
  // 5
  prefs.remove(_generateKey(userId, key));
}

Here’s what you have done:

  1. First, convert object into a string using JsonEncoder. This encoder makes a String out of a Map.
  2. Then set that serialized object into the store.
  3. When getting an object, fetch the string using the generated key.
  4. Next, if there was a string, convert that into a Map, else return null.
  5. Lastly, when removing an object, remove using the generated key.

Build and run your project. It should now restore your cart items when you restart, or even when you logout and login. Hooray!

Do you stop now? No? Good, you persist for more learning.

Persisting Data in Disk with Files

In order to save strings, images, and objects to files, you need to serialize them as before. For strings and objects, they can be serialized to strings then saved to a file. The location of the file can depend on the user’s id and a key. The images, however can be saved directly on disk.

Flutter provides a File class. But before writing to a file, you need to get a reference to the location where you will write files. For that reason, you need to get the location of a directory, e.g. the documents directory or the temporary or another directory, of the platform you are running on.

In order to do that, you can use Flutter’s path_provider plugin. This plugin makes it possible to get the documents or temporary directory based on the platform you are running. On the other hand, reading files is the same once you have the location of the file. You can use Flutter’s File class to read the contents as a byte array.

Now that you know some theory, you should write some code. :]

But first, open main.dart and replace the repository parameter in Storage.create to use FilePersistence instead of LocalKeyValuePersistence:

Storage.create(repository: FilePersistence())