Persistent Data
Learning Objectives
- You understand the need to persist data between sessions.
- You know of the
hive_ce
andhive_ce_flutter
libraries and know how to use them to store and retrieve data. - You can create an application that stores and retrieves data using
hive_ce
andhive_ce_flutter
.
By default, when we close an application, all the data stored in the application is lost, which is not always desirable. For example, for an application that authenticates the user, it can be meaningful to store authentication information so that the user does not need to authenticate every time the application is opened. Similarly, for an application that requires remembering information, such as a task manager or some other tool, storing data is a requirement.
Persistent data means that the data is stored on the device and is available even after the application is closed and reopened. In this section, we explore how to store data in a Flutter application so that it persists between sessions, focusing on two libraries called hive_ce
and hive_ce_flutter
.
Using hive_ce and hive_ce_flutter
To use hive_ce
and hive_ce_flutter
, they needs to be added to the dependencies in the pubspec.yaml
file of the project, and imported to the Dart files where it is used.
dependencies:
# new lines:
hive_ce: 2.5.0+2
hive_ce_flutter: 2.0.0
The hive_ce
library provides a key-value database that can be used to store and retrieve data. The hive_ce_flutter
library provides Flutter-specific functionality for using hive_ce
in a Flutter application.
The
hive_ce
library is a community-maintained version of hive that has not been updated recently. Thehive_ce
works as a drop-in replacement for Hive. From hereon, we use the term “Hive” when discussing the library.
Hive is a key-value database that can be used for storing and reading data. In a Dart application, the database would be used as follows. Note that the main
function is now asynchronous, and the return type is Future<void>
. This is because opening the storage is an asynchronous operation.
import 'package:hive_ce/hive.dart';
Future<void> main() async {
// create a storage
final storage = await Hive.openBox("storage");
// write content to the storage
storage.put('key', 'value');
// read content from the storage
final value = storage.get('key');
// print the content to console
print(value);
}
The output of the above program is “value”, which is the value that was stored with the key “key”. Hive uses the term “box” for the storage, and the openBox
method is used to open a storage — once a storage has been opened once using openBox
, it can be referred to with the method box
. The put
method is used to store data in the storage, and the get
method is used to retrieve data from the storage. The data stored in the storage is persistent and is available even after the application is closed and reopened.
Storage in a Flutter application
To use Hive in a Flutter application, we need to import the hive_ce_flutter
package, initiate Hive for Flutter in the main function, and create a storage. The following outlines a main function that initiates Hive for Flutter and opens a storage. When opening a storage with the method openBox
, we pass the method a string. The string acts as a key that identifies the storage.
// other imports
import 'package:hive_ce_flutter/hive_flutter.dart';
Future<void> main() async {
await Hive.initFlutter();
await Hive.openBox("storage");
// running Flutter application
}
In these examples and in the following exercises, we assume that we use the name “storage” for the storage. The name can be any string, but it is important to use the same name when referring to the storage elsewhere in the application.
When considering our earlier Flutter application that kept track of likes, we could use Hive to store the number of likes. Let’s look into modifying the application so that the likes are persisted. After taking Hive into use, the main function would be as follows.
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hive_ce_flutter/hive_flutter.dart';
Future<void> main() async {
await Hive.initFlutter();
await Hive.openBox("storage");
Get.lazyPut<CountController>(() => CountController());
runApp(
GetMaterialApp(
home: ClickCounterView(),
),
);
}
Now, we can use Hive elsewhere in the application. To use Hive for storing and retrieving the number of likes, we would need to modify the controller class. The key changes would include adding the storage as a property to the controller class, initializing the value of the count
variable with the value stored in the storage, and storing the new value in the storage when the value of count
changes.
The following outlines the class CountController with the changes.
// dependencies
class CountController {
final storage = Hive.box("storage");
var count;
CountController(): count = 0.obs {
count.value = storage.get('count') ?? 0;
}
void increment() {
count++;
storage.put('count', count.value);
}
}
The constructor initiates the count
variable by creating a reactive variable with an initial value of 0. Then, the code in the block is executed. The count.value = storage.get('count') ?? 0;
line reads the value stored in the box with the key count
, returning it if it exists, and 0 otherwise. This way, the value of count
is initialized with the value stored in the box.
The
value
property of the reactive variable allows setting and retrieving the value, while still keeping the variable reactive.
As a whole, an application using Hive for storing the number of likes would be as follows. The key changes include importing the hive_ce_flutter
package, initiating Hive and opening up a storage in the main function, and using Hive in the controller class.
Service for data management
When the application grows, it becomes meaningful to separate data management from the controller. This allows for better organization of the code and makes it easier to test and maintain the application. For our above application, we could create a separate class called CountService
that would have the storage as a property and methods for reading and writing data. The CountController
class would then use the CountService
class to store and retrieve the number of likes.
The following outlines how the CountService
class could be implemented. The class has an instance of Hive called storage
, and the methods for reading and writing data. This could be implemented as follows — the int get count
is a getter that returns the value stored in the box with the key count
, or 0 if it is not found, and the void increment
method increases the value of count
by one.
// imports
class CountService {
final storage = Hive.box("storage");
int get count => storage.get('count') ?? 0;
void increment() {
storage.put('count', count + 1);
}
}
Now, to allow sharing the service between different parts of the application, we would need to register the CountService
class as a dependency in the application. Similar to earlier, this would be done using Get.lazyPut
in the main function.
// imports
Future<void> main() async {
await Hive.initFlutter();
await Hive.openBox("storage");
Get.lazyPut<CountController>(() => CountController());
Get.lazyPut<CountService>(() => CountService());
runApp(
GetMaterialApp(
home: ClickCounterView(),
),
);
}
With the CountService
class in place, the CountController
class could be modified to use the service for storing and retrieving the number of likes. The updated CountController
would be as follows.
// imports
class CountController {
final service = Get.find<CountService>();
var count;
CountController(): count = 0.obs {
count.value = service.count;
}
void increment() {
count++;
service.increment();
}
}
As a whole, the application with the service would look as follows.
Storing complex data
Hive can store simple data types such as strings, integers, booleans, lists, and maps. As an example, the following code snippet shows how to store and retrieve a list of strings.
import 'package:hive_ce/hive.dart';
Future<void> main() async {
// create a storage
final storage = await Hive.openBox("storage");
// write content to the storage
storage.put('key', ['value1', 'value2']);
// read content from the storage
final value = storage.get('key');
// print the content to console
print(value);
}
The output of the above program is ['value1', 'value2']
, which is the list of strings that was stored with the key key
.
Similarly, the program below shows how to store and retrieve a map of strings.
import 'package:hive_ce/hive.dart';
Future<void> main() async {
// create a storage
final storage = await Hive.openBox("storage");
// write content to the storage
storage.put('key', {'key1': 'value1', 'key2': 'value2'});
// read content from the storage
final value = storage.get('key');
// print the content to console
print(value);
}
The output of the above program is {key1: value1, key2: value2}
, which is the map of strings that was stored with the key key
.
If we would like to store more complex data, such as a list of objects, we would need to introduce a TypeAdapter for converting the objects to a serializable format specific to Hive. Alternatively, we could create JSON-formatted strings from each of the object and store the strings in a list or map. The following example demonstrates how to store and retrieve a list of objects using Hive without the type adapter functionality.
import 'package:hive_ce/hive.dart';
class Person {
final String name;
final int yearOfBirth;
Person(this.name, this.yearOfBirth);
Map toJson() => {
'name': name,
'yearOfBirth': yearOfBirth,
};
factory Person.fromJson(Map json) {
return Person(json['name'], json['yearOfBirth']);
}
}
Future<void> main() async {
final storage = await Hive.openBox("storage");
final persons = [
Person("Jean Sibelius", 1865),
Person("Alvar Aalto", 1898),
];
// store the list as a list of JSON-formatted strings
storage.put(
'key',
persons.map((person) => person.toJson()).toList(),
);
// read the list and transform the JSON-formatted strings back to person objects
final list = storage.get('key').map((json) => Person.fromJson(json)).toList();
for (var person in list) {
print("${person.name} (${person.yearOfBirth})");
}
}
The above example correctly outputs the names and years of birth of the persons stored and retrieved using Hive. The above example also demonstrates how to convert objects to and from JSON-formatted strings. The toJson
method converts the object to a JSON-serializable map, while the fromJson
factory constructor creates an instance of the object from a JSON-serializable map.
Other storage options
While we looked into Hive in this section, there exists a wide variety of ways for storing information in a Flutter application. The options range from working with files to using external services such as Firebase.
There are also other libraries for the same purpose as Hive. Some of the other popular libraries include:
- shared_preferences: A Flutter plugin for reading and writing simple key-value pairs.
- sqflite: A Flutter plugin for SQLite.
- get_storage: A simple key-value database written in Dart.