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?
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:
- Convert the image into a Base64 string.
- Save the string using the generated key.
- Return the key as the identifier used to save the image.
- When getting an image, get the Base64 string of the image using
key
. - If that was not null, convert it back to a byte array.
- 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:
- First, convert
object
into a string usingJsonEncoder
. This encoder makes aString
out of aMap
. - Then set that serialized object into the store.
- When getting an object, fetch the string using the generated key.
- Next, if there was a string, convert that into a
Map
, else returnnull
. - 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())