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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Data Persistence on Flutter
30 mins
- Getting Started
- What Kind of Persistence Can You Use
- Serialized Data and Encryption
- Saving Data to Memory
- Flutter Persistence Options
- Persisting Data in a Key-Value Store
- Saving Plain Text with Key-Value Store
- Reading Plain Text with Key-Value Store
- Swapping out the Persistence Layer
- Removing Plain Text with Key-Value Store
- Saving Image with Key-Value Store
- Saving Objects into a Key-Value Store
- Persisting Data in Disk with Files
- Saving Your Data to Files
- Getting Strings, Images, and Objects from Files
- Persisting Data Online
- Saving Your Data Online
- Reading Your Data Online
- Removing Your Data Online
- Where to Go From Here?
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:
- First, when saving an image, get a filename for the image using the user’s id, the type ‘images’, and a key.
- Then, get a file reference.
- 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. - Save the image to the file as bytes and return the filename.
- When saving an object, convert the object into a string using
JsonEncoder
and write that to the file as a string. - 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:
- First, when getting an image, if the file exists, read the file as bytes and return it. Otherwise, return null.
- When getting a string, if the file exists, read the file as a string and return it. Otherwise, return null.
- 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.
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.
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:
- Imports the
device_info
package. - Get a reference to the device info plugin.
- Next, if it’s Android return the
androidId
insideandroidInfo
. - If it’s iOS return the
identifierForVendor
insideiosInfo
.
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())