Choose the Best Data Storage Solution for Your Flutter App

Flutter category image

Introduction

Learn how to choose the best data storage solution for your Flutter app between local storage, secure storage, or a cloud database. With this guide, they won’t be any open questions about what’s the correct approach for your app!

Depending of what type of app you develop, you might need different data storage solutions. In this article, I’ll introduce various options for Flutter developers of how to manage data storage in your apps. Afterwards, you will be able to choose the best data storage solution for your Flutter app. The following topics will be covered:

  • Local storage
  • Secure storage
  • Cloud storage

In this article, we will distinguish between two use cases for databases: storing large data blobs such as PDF files or images, and storing small key-value pairs. Whatever your needs may be, there is a solution available for you.

Local storage

Local storage means that data is only stored on the user’s device. This is useful for apps that don’t require a permanent internet connection, or perform all operations on the device itself.

Localstore is a persistent document store for many small JSON files. To store large data chunks, it is better to write directly to files.. If you need a key-value store, there is Shared Preferences or Hive available for you. Every approach presented here will store the data persistently, so that it is available again when the user launches the app.

Let’s dive into some code examples!

Localstore

Localstore is a persistent NoSQL database based on JSON files. It is suitable for smaller JSON files, but not for larger ones. In such cases, it is better to read and write the files directly, without using a database.

// create an instance to access the database
final db = Localstore.instance;
// get a new document id
final id = db.collection('todos').doc().id;
// save a new item in the todos collection with the provided id
db.collection('todos').doc(id).set({
'title': 'Todo title',
'done': false
});
// get an item with the provided id from the todos collection
final data = await db.collection('todos').doc(id).get();
// get all items from the todos collection
final items = await db.collection('todos').get();
// delete an item with the given id from the todos collection
db.collection('todos').doc(id).delete();

Easy and simple approach to handle multiple small JSON files in your app!

Reading and writing files

The most basic approach for data storage in Flutter is to read and write files. In fact, every package that uses persistent storage does this in the background, although you may not notice it.

Reading and writing files in Flutter is not difficult. However, it does require more code than using a package. On the other hand, you have more control over the process. Simply import dart:io and you’re ready to go. Here are the steps:

import 'dart:async';
import 'dart:io';
import 'package:path_provider/path_provider.dart';
Future<String> get _localPath async {
// package path_provider returns the correct path on all platforms
final directory = await getApplicationDocumentsDirectory();
return directory.path;
}
Future<File> get _localFile async {
final path = await _localPath;
return File('$path/counter.txt');
}
Future<int> readCounter() async {
try {
final file = await _localFile;
// Read the file
final contents = await file.readAsString();
return int.parse(contents);
} catch (e) {
// If encountering an error, return 0
return 0;
}
}
Future<File> writeCounter(int counter) async {
final file = await _localFile;
// Write the file
return file.writeAsString('$counter');
}

Yes, it is more code and you most likely want to write a wrapper in your app, but you have the maximum amount of control with this approach.

SharedPreferences

Shared Preferences is a useful tool for persisting small amounts of data, such as user settings. It’s a key-value store similar to a Dictionary in Dart. The API is easy to learn, with getter and setter methods for various types like Int, Double, Bool, String, and StringList.

I recommend storing only user or app settings in Shared Preferences. Create a class for all settings, implement a toJson() method, and use jsonEncode() to generate a string from it. Then save it with setString() and load it with getString() whenever you need it. If you are not familiar with JSON handling in Dart, have a look at this tutorial.

Here is a code example showing how to read and write data with the Shared Preferences package:

Future _sharedPreferencesExample() async {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool("myBool", true);
await prefs.setDouble("myDouble", 1.23);
await prefs.setInt("myInt", 4);
await prefs.setString("myAppSettings", jsonEncode(userSettings.toJson()));
if (prefs.containsKey("myAppSettings")) {
print(prefs.getString("myAppSettings"));
}
final allKeyCount = prefs.getKeys().length;
await prefs.remove("myAppSettings");
prefs.clear();
}

For simple use cases, Shared Preferences might be a viable solution. However, serious app developers often prefer other approaches.

Hive

Hive is a lightweight and blazing fast key-value database written in pure Dart. It works on all platforms, is simple to use, and has no native dependencies. Similar to Shared Preferences, you can use Hive as a Dictionary. However, a key-value store is called Box. The process is simple:

The first step is always to initialize Hive. This needs to be done exactly once as long as the app runs. For Flutter apps, just add the code await Hive.initFlutter(); somewhere before you access a box for the first time.

You can store any primitive data in a box. Here’s an example:

var box = Hive.box('myBox');
box.put('name', 'David');
var name = box.get('name');
print('Name: $name');

With a type adapter, you can even store custom objects in your database. The class must derive from HiveObject and the properties must be annotated. Then, you can also benefit from save() and delete() methods!

@HiveType(typeId: 0)
class Person extends HiveObject {
@HiveField(0)
String name;
@HiveField(1)
int age;
}
...
var box = await Hive.openBox('myBox');
var person = Person()
..name = 'Dave'
..age = 22;
box.add(person);
print(box.getAt(0)); // Dave - 22
person.age = 30;
person.save();
print(box.getAt(0))

If your boxes contain a lot of data and you want lazy loading, use a LazyBox. For higher security, Hive also provides EncryptedBoxes with AES 256 CBC and PKCS7 padding.

Hive has everything that a key-value database needs to offer. If you are still unsure whether Hive is the right choice for your next project, read these hints on when to use Hive and when not to use it.


Want More Flutter Content?

Join my bi-weekly newsletter that delivers small Flutter portions right in your inbox. A title, an abstract, a link, and you decide if you want to dive in!


Secure storage

To store data securely, you can use an encrypted version of Shared Preferences called Flutter Secure Storage. Alternatively, Hive also offers encrypted boxes. These work similarly to the default boxes in the previous example, but with the added benefit of secure storage at the cost of speed.

Flutter Secure Storage

Flutter Secure Storage is a useful tool for storing data securely. It is an encrypted version of Shared Preferences and allows you to store sensitive data such as user authentication tokens, API keys, and other confidential information. It provides a read and write method for any type of data, and it is easy to use with a simple and intuitive API.

This package uses the secure storage of the host system and is compatible with iOS, Android, MacOS, Linux, Windows, and the Web platform. Please refer to the documentation for instructions on how to configure the package for each platform.

Here is a code example to get you started:

import 'package:flutter_secure_storage/flutter_secure_storage.dart';
// Create storage
final storage = new FlutterSecureStorage();
// Read value
String value = await storage.read(key: key);
// Read all values
Map<String, String> allValues = await storage.readAll();
// Delete value
await storage.delete(key: key);
// Delete all
await storage.deleteAll();
// Write value
await storage.write(key: key, value: value);

Remember to add flutter_secure_storage to your pubspec.yaml file before using it.

Check out the Flutter Secure Storage documentation for more details and options.

Hive with encrypted boxes

We previously discussed Hive. One major benefit is that it also provides encrypted boxes to securely store your data.

All you need is an encryption key that Hive can generate for you. You need to store that encryption key in a safe location. The following code example uses Flutter Secure Storage, but you can also use other packages or methods if you prefer.

Let’s have a look at the code:

import 'dart:convert';
import 'package:hive/hive.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
void main() async {
const secureStorage = FlutterSecureStorage();
// if key not exists return null
final encryptionKeyString = await secureStorage.read(key: 'key');
if (encryptionKeyString == null) {
final key = Hive.generateSecureKey();
await secureStorage.write(
key: 'key',
value: base64UrlEncode(key),
);
}
final key = await secureStorage.read(key: 'key');
final encryptionKeyUint8List = base64Url.decode(key!);
print('Encryption key Uint8List: $encryptionKeyUint8List');
final encryptedBox = await Hive.openBox('vaultBox', encryptionCipher: HiveAesCipher(encryptionKeyUint8List));
encryptedBox.put('secret', 'Hive is cool');
print(encryptedBox.get('secret'));
}

🔔 Hint

  • Only values are encrypted, keys are stored in plain text
  • If you use a wrong encryption key, you may get confusing results. There is no check if a key is correct!

Storing the encryption key in a secure location can make this solution somewhat cumbersome.


Cloud storage

When choosing the best data storage solution for your Flutter app, you should always consider cloud-based storage options. A lot of Flutter developers use Firebase as their cloud solution due to its easy integration with Flutter apps. Firebase provides a NoSQL database called Firestore to store JSON documents, as well as a Storage that acts as a file store.

Firestore

Firestore is a NoSQL document database and part of the Google Firebase services. It uses a hierarchy of collections and documents to structure data and is optimized for large collections of small documents. It is not useful, or even possible, to store entire files in it.

The Flutter package for Firestore uses well-known methods like get, update, set, and delete to manipulate data. Here is some example code on how to deal with it.

import 'package:cloud_firestore/cloud_firestore.dart';
class Firestore {
// just a simple get
static Future<List<Car>> getAllEntries(String collection) async {
return (await FirebaseFirestore.instance.collection(collection).get())
.docs
.map((item) => Car.fromMap(item.data()))
.toList();
}
// get with custom order
static Future<List<Car>> getAllEntriesSortedByName(String collection) async {
return (await FirebaseFirestore.instance
.collection(collection)
.orderBy("manufacturer", descending: false)
.get())
.docs
.map((item) => Car.fromMap(item.data()))
.toList();
}
// get with filter
static Future<List<Car>> getAllEntriesFilteredByPrice(
String collection) async {
return (await FirebaseFirestore.instance
.collection(collection)
.where("price", isGreaterThan: 60000)
.get())
.docs
.map((item) => Car.fromMap(item.data()))
.toList();
}
static Future addEntryWithAutogeneratedId(
String collection, Map<String, dynamic> data) async {
await FirebaseFirestore.instance.collection(collection).add(data);
}
// updates an existing entry (missing fields won't be touched on update), document must exist
static Future updateEntryWithId(
String collection, String documentId, Map<String, dynamic> data) async {
await FirebaseFirestore.instance
.collection(collection)
.doc(documentId)
.update(data);
}
// adds or updates an existing entry (missing fields will be deleted on update!), document will be created if needed
static Future addOrUpdateWithId(
String collection, String documentId, Map<String, dynamic> data) async {
await FirebaseFirestore.instance
.collection(collection)
.doc(documentId)
.set(data);
}
// deletes the entry with the given document id
static Future deleteEntry(String collection, String documentId) async {
await FirebaseFirestore.instance
.collection(collection)
.doc(documentId)
.delete();
}
}

To get a quick start with Firestore, check out my detailed article below:

Flutter ❤️ Firebase

Get started with Firebase and learn how to use it in your Flutter apps. My detailed ebook delivers you everything you need to know! Flutter and Firebase are a perfect match!

Storage

Storage is a blob storage service and part of the Google Firebase services. It is useful when you want to store PDF, images, or text files in a cloud environment. With its Flutter package, you can easily upload and download files as you like. Have a look at the following code examples.

Get a storage reference (file pointer)

// creating references
final ref = FirebaseStorage.instance.ref();
final ref2 = FirebaseStorage.instance.ref("images/myImage.jpg");
// Google Cloud Storage URI, replace <YOUR_BUCKET>
final ref3 = FirebaseStorage.instance
.refFromURL("gs://<YOUR_BUCKET>/images/myImage.jpg");
// HTTPS URL, replace <YOUR_BUCKET>, escape url characters
final ref4 = FirebaseStorage.instance.refFromURL(
"https://firebasestorage.googleapis.com/b/<YOUR_BUCKET>/o/images%20myImage.jpg");
// navigating through the reference tree
final ref5 = ref.child("images").child("myImage.jpg");
final ref6 = ref.parent;
final ref7 = ref.root;

Read data

static Future<List<FileData>> getData() async {
final ref = FirebaseStorage.instance.ref();
var files = await ref.list(const ListOptions(maxResults: 5));
var res = <FileData>[];
for (var item in files.items) {
var content = await item.getData();
var created = (await item.getMetadata()).timeCreated!;
res.add(FileData(content!, item.name, created, item));
}
return res;
}

Write data

static UploadTask uploadItem(File file, String targetName) {
final ref = FirebaseStorage.instance.ref();
var child = ref.child(targetName);
// With the UploadTask object you can listen to progress changes and control
// the upload
return child.putFile(file);
}

For more details about Firebase Storage, read my detailed article below:

Flutter ❤️ Firebase

Get started with Firebase and learn how to use it in your Flutter apps. My detailed ebook delivers you everything you need to know! Flutter and Firebase are a perfect match!

Conclusion

In this article, you learned how to choose the best data storage solution for your Flutter app. In conclusion, there are various data storage solutions available for Flutter developers, depending on the type of app being developed. This article covered local storage, secure storage, and cloud storage options, as well as use cases for storing large data blobs and small key-value pairs. With the appropriate storage solution, developers can ensure that their app’s data is managed efficiently and securely.


Want More Flutter Content?

Join my bi-weekly newsletter that delivers small Flutter portions right in your inbox. A title, an abstract, a link, and you decide if you want to dive in!

Flutter ❤️ Firebase

Get started with Firebase and learn how to use it in your Flutter apps. My detailed ebook delivers you everything you need to know! Flutter and Firebase are a perfect match!

Become A Testing Expert!

Become a proficient Flutter app tester with my detailed guide. This ebook covers everything from unit tests over widget tests up to dependency mocking.