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 3 of 4 of this article. Click here to view the first page.

Saving Your Data to Files

When saving to a file, you need a reference to the file. You want to save inside the document directory. Open lib/data/FilePersistence.dart and add the following code:

// 1
import 'dart:io';
import 'package:path_provider/path_provider.dart';

class FilePersistence implements Repository {
  // 2
  Future<String> get _localPath async {
    final directory = await getApplicationDocumentsDirectory();
    return directory.path;
  }

  // 3
  Future<File> _localFile(String filename) async {
    final path = await _localPath;
    return File('$path/$filename');
  }

  ...
}

First, you import dart:io package that has the File class and the path_provider plugin that contains getApplicationDocumentsDirectory. Then you declare a method _localPath that returns the document path as a string. Next, you declare a _localFile method that makes a File object from a filename inside the documents directory.

You also need to have a method that will generate the filename using all the information you have, so add the following:

Future<String> getFilename(String userId, String type, String key) async {
  return userId + '/' + type + '/' + key;
}

Here you generate a path userId/type/key. For example, an object with the key cart for the userId 133t will be in l33t/object/cart file.

Now you are ready to add the persistence methods. Add the following save methods to FilePersistence.dart:

@override
Future<String> saveImage(String userId, String key, Uint8List image) async {
  // 1
  final filename = await getFilename(userId, 'images', key);
  // 2
  final file = await _localFile(filename);

  // 3
  if (!await file.parent.exists()) await file.parent.create(recursive: true);

  // 4
  await file.writeAsBytes(image);
  return filename;
}

@override
void saveObject(
    String userId, String key, Map<String, dynamic> object) async {
  final filename = await getFilename(userId, 'objects', key);
  final file = await _localFile(filename);

  if (!await file.parent.exists()) await file.parent.create(recursive: true);

  // 5
  final jsonString = JsonEncoder().convert(object);
  await file.writeAsString(jsonString);
}

@override
void saveString(String userId, String key, String value) async {
  final filename = await getFilename(userId, 'strings', key);
  final file = await _localFile(filename);

  if (!await file.parent.exists()) await file.parent.create(recursive: true);

  // 6
  await file.writeAsString(value);
}

You’ll need to add an import for JsonEncoder using the same keystroke as earlier.

With that, you have set up all the necessary things required to save to a file. Here’s what you have done:

  1. First, when saving an image, get a filename for the image using the user’s id, the type ‘images’, and a key.
  2. Then, get a file reference.
  3. Next, if the file’s parent directory does not exist, create it. Using true for the argument creates all the parent directories if they don’t exist.
  4. Save the image to the file as bytes and return the filename.
  5. When saving an object, convert the object into a string using JsonEncoder and write that to the file as a string.
  6. Lastly, when saving a string, write that string to the file.

Getting Strings, Images, and Objects from Files

Before you see any results when you build and run, you need to implement the get methods. Add the following to FilePersistence:

@override  
Future<Uint8List> getImage(String userId, String key) async {
  final filename = await getFilename(userId, 'images', key);
  final file = await _localFile(filename);

  // 1
  if (await file.exists()) return await file.readAsBytes();
  return null;
}

@override
Future<String> getString(String userId, String key) async {
  final filename = await getFilename(userId, 'strings', key);
  final file = await _localFile(filename);

  // 2
  if (await file.exists()) return await file.readAsString();
  return null;
}

@override
Future<Map<String, dynamic>> getObject(String userId, String key) async {
  final filename = await getFilename(userId, 'objects', key);
  final file = await _localFile(filename);

  // 3
  if (await file.exists()) {
    final objectString = await file.readAsString();
    return JsonDecoder().convert(objectString);
  }
  return null;
}

Here’s what you did:

  1. First, when getting an image, if the file exists, read the file as bytes and return it. Otherwise, return null.
  2. When getting a string, if the file exists, read the file as a string and return it. Otherwise, return null.
  3. Lastly, when getting an object, if the file exists, read it as a string. Then convert that string into a map using JsonDecoder. Otherwise, return null.

Build and run the application, and now you should be able to see your login and shopping cart survive an app restart. This time, with file persistence as your repository instead of a key-value store.

However, the remove methods are still empty. Without these, even if you logout, you are still logged in when you restart the app. You should implement them as an additional exercise. Implementing the remove methods will allow you to logout and see the login screen after restarting.

Persisting Data Online

At this point, you’ve tried two types of disk persistence in your project. However, say you want to access your cart data on another device by logging in. In order to do that, you should persist your data online over the network. One way to persist data online is using Google’s Firestore.

Before doing so, you need to setup Firebase in your project, for both iOS and Android. You can follow the setup steps here for Flutter. But it also includes steps to setup for both iOS and Android. Make sure you have updated the google-services.json for the Android project and the GoogleService-Info.plist file for iOS. You can do that by following the steps here.

Also, make sure to create the Cloud Firestore database and set the rules properly on the Rules tab in the console for the database. You can see an example of the rules that enables global writing below.

Note: Global writes are insecure so this is only intended for testing purposes.
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, write;
    }
  }
}

Another Firebase service you will use is Firebase Storage. Make sure to follow the setup here to create a default bucket. Then set the rules like below for global write access, just for development.

Note: Global writes are insecure so this is only intended for testing purposes.
rules_version = '2';
service firebase.storage {
  match /b/{bucket}/o {
    match /{allPaths=**} {
      allow read, write: if true;
    }
  }
}

Another question you might have is, how do you recover your logged in session when using purely cloud persistence. That’s a good question. Without saving your login session information to disk, your other option is to store the login session information to the cloud using another user identifier.

One identifier that fits this purpose is the device ID for android and iOS. Open lib/Storage.dart and look at the deviceId method:

// 1
import 'package:device_info/device_info.dart';

Future<String> deviceId() async {
  // 2
  final DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
  if (Platform.isAndroid) {
    // 3
    final AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo;
    return await androidInfo.androidId;
  } else {
    // 4
    final IosDeviceInfo iosInfo = await deviceInfo.iosInfo;
    return await iosInfo.identifierForVendor;
  }
}

Here’s what that method does:

  1. Imports the device_info package.
  2. Get a reference to the device info plugin.
  3. Next, if it’s Android return the androidId inside androidInfo.
  4. If it’s iOS return the identifierForVendor inside iosInfo.

Now that you understand how the login information is persisted for your device, you can start writing the persistence methods.

But first, open main.dart and replace the repository parameter in Storage.create with a CloudPersistence object:

Storage.create(repository: CloudPersistence())