Hive for Flutter — fast local storage database made with Dart

awaik
7 min readJun 21, 2020

About a month ago, talking with one application developer on Flutter, there was a problem of braking the processing of small (in tens of thousands) data arrays on the user’s phone.

Many applications require data processing on the phone and, further, their synchronization with the backend. For example to-do lists, lists of any data (analyzes, notes, etc.).

It’s not at all cool when the list of just a few thousand items, when deleting one of them and then writing to the cache or searching the cache, starts to slow down.

There is a solution! Hive — NoSQL database is written in pure Dart, very fast. In addition, the advantages of Hive:
* Cross-platform — since there are no native dependencies on pure Dart — mobile, desktop, browser.
* High performance.
* Built-in strong encryption.

In the article, we will look at how to use Hive and make a simple ToDo application, which in the next article will complement authorization and synchronization with the cloud.

Example

Here we will write such an application at the end of the article, the code is posted on Github.

One of the advantages of Hive is the very good documentation https://docs.hivedb.dev/ — in theory, everything is there. In the article, I will simply briefly describe how to work with what and make an example.
So, to connect hive to the project, add to pubspec.yaml

dependencies:
hive: ^1.4.1+1
hive_flutter: ^0.3.0+2
dev_dependencies:
hive_generator: ^0.7.0+2
build_runner: ^1.8.1

Next, init it, usually when the app starts.

await Hive.initFlutter();

In case we are writing an application for different platforms, we can do initialization with a condition. Hive_flutter package:^0.3.0+2 is just a service wrapper making working with Flutter easier.

Data types

Out of the box, Hive supports the List, Map, DateTime, BigInt, and Uint8List data types.
Also, you can create your own types and work with them using the TypeAdapters adapter. Adapters can be done by yourself https://docs.hivedb.dev/#/custom-objects/type_adapters or use the built-in generator (we connect it with this line hive_generator:^0.7.0+2).
For our application, we need a class for storing

class Todo {
bool complete;
String id;
String note;
String task;
}

Modify it for generator and give id number typeId: 0

import 'package:hive/hive.dart';
part 'todo.g.dart';
@HiveType(typeId: 0)
class Todo {
@HiveField(0)
bool complete;
@HiveField(1)
String id;
@HiveField(2)
String note;
@HiveField(3)
String task;
}

If in the future we need to expand the class, it is important not to disturb the order, but to add new properties further by numbers. Unique property numbers are used to identify them in the Hive binary format.

Now run the command

flutter packages pub run build_runner build --delete-conflicting-outputs

and get the generated class for our data type todo.g.dart

Now we can use this type to write and get objects of this type to \ from Hive.

HiveObject — a class to simplify object management We can add convenient methods to our Todo data type by simply inheriting it from the built-in HiveObject class

class Todo extends HiveObject {

It gives us access for two methods save() and delete(), that sometimes very convenient.

Organising data — Box

In Hive, data is stored in the boxes. This is very convenient since we can make different boxes for user settings, user interface, etc.

The Box is identified by a string. To use it, you must first open it asynchronously (this is a very fast operation). In our version

var todoBox = await Hive.openBox<Todo>('box_for_todo');

and then we can synchronously read / write data from this box.

Identification of data in the box is possible either by key or by serial number:

For example, open a box with string data and write data by key and with auto-increment

var stringBox = await Hive.openBox<String>('name_of_the_box');

By key

stringBox.put('key1', 'value1');
print(stringBox.get('key1')); // value1
stringBox.put('key2', 'value2');
print(stringBox.get('key2')); // value2

Autoincrement when save + access data by index

To record many objects, it is convenient to use boxing similarly to List. All objects in the box have an index of type autoincrement.

The methods are designed to work with this: getAt(), putAt() and deleteAt()

For the record, we simply use add () without an index.

stringBox.put('value1');
stringBox.put('value2');
stringBox.put('value3');
print(stringBox.getAt(1)); //value2

Why is it possible to work synchronously? Developers write that this is one of the strengths of Hive. When you request a recording, all Listeners are notified immediately, and recording takes place in the background. If a recording fails (which is very unlikely and in theory, you can not handle this), then Listeners are notified again. You can also use await for work.

When the box is already open, then anywhere in the application we call it
var stringBox = await Hive.box<String>('name_of_the_box');
from which it immediately becomes clear that when creating a box, the package creates singleton.

Therefore, if we do not know whether boxing is already open or not, and to check laziness, then we can use it in doubtful places
var stringBox = await Hive.openBox<String>('name_of_the_box');

  • if the box is already open, this call will simply return the instance of the already open box.

If you look at the source code of the package, you can see several helper methods:

/// Returns a previously opened box.
Box<E> box<E>(String name);
/// Returns a previously opened lazy box.
LazyBox<E> lazyBox<E>(String name);
/// Checks if a specific box is currently open.
bool isBoxOpen(String name);
/// Closes all open boxes.
Future<void> close();

Devhack In general, since Flutter is a complete open source, you can climb into any methods and packages, which is often faster and more understandable than reading the documentation.

LazyBox — for big data sets

When we create a regular box, all its contents are stored in memory. This gives high performance. In such boxes, it is convenient to store user settings, some small data.
If there is a lot of data, it is better to create boxes lazily

var lazyBox = await Hive.openLazyBox('myLazyBox');
var value = await lazyBox.get('lazyVal');

When it is opened, Hive reads the keys and stores them in memory. When we request data, Hive knows the position of the data on the disk and quickly reads it.

Box Encryption

Hive supports AES-256 box data encryption.
To create a 256-bit key, we can use the built-in function
var key = Hive.generateSecureKey();
which creates the key using the Fortuna random number generator.

After key created we create the box

var encryptedBox = await Hive.openBox('vaultBox', encryptionKey: key);
encryptedBox.put('secret', 'Hive is cool');
print(encryptedBox.get('secret'));

Features:

  • Only values are encrypted, keys are stored as plaintext.
  • When you close the application, you can store the key using the flutter_secure_storage package or use your own methods.
  • There is no built-in key validation check, therefore, in the case of a wrong key, we must program the application behavior ourselves.

Box Compression

As usual, if we delete or change data, they are written incrementally at the end of the box.

We can do compression, for example, when closing the box when exiting the application

var box = Hive.box('myBox');
await box.compact();
await box.close();

The app todo_hive_example

Ok, that’s all, we’ll write an application that we will expand later to work with the backend.

We already have a data model, we’ll make the interface simple.

Screens:

  • Home screen — to-do list + all functionality
  • Add Case Screen

Actions:

  • + Button adds business
  • Clicking on the checkmark — switching done / not done
  • Swipe Anyway — Delete

Creating an Application Step 1 — Add

We create a new application, delete comments, leave the entire structure (we need a + button.

We put the data model in a separate folder, run the command to create the generator.

Create a list of common tasks.

To build a to-do list, we use the built-in extension (yes, we added extensions (extensions) to Dart a couple of months ago), which is located here /hive_flutter-0.3.0+2/lib/src/box_extensions.dart

/// Flutter extensions for boxes.
extension BoxX<T> on Box<T> {
/// Returns a [ValueListenable] which notifies its listeners when an entry
/// in the box changes.
///
/// If [keys] filter is provided, only changes to entries with the
/// specified keys notify the listeners.
ValueListenable<Box<T>> listenable({List<dynamic> keys}) =>
_BoxListenable(this, keys?.toSet());
}

So, we create a to-do list, which itself will be updated when the box changes

body: ValueListenableBuilder(
valueListenable: Hive.box<Todo>(HiveBoxes.todo).listenable(),
builder: (context, Box<Todo> box, _) {
if (box.values.isEmpty)
return Center(
child: Text("Todo list is empty"),
);
return ListView.builder(
itemCount: box.values.length,
itemBuilder: (context, index) {
Todo res = box.getAt(index);
return ListTile(
title: Text(res.task),
subtitle: Text(res.note),
);
},
);
},
),

While the list is empty. Now when you press the + button, we’ll add the case.

To do this, create a screen with a form onto which we throw over when you press the + button.

On this screen, when you click the Add button, we call the code that adds the record to the box and throws it back to the main screen.

void _onFormSubmit() {
Box<Todo> contactsBox = Hive.box<Todo>(HiveBoxes.todo);
contactsBox.add(Todo(task: task, note: note));
Navigator.of(context).pop();
}

Everything, the first part is ready, this application is already able to add Todo. When you restart the application, all data is saved in Hive.

The link to the commit on the Github applications at this point.

Application Creation Step 2 — Switching done / not done

It’s all very simple. We use the save method that we inherited from our class Todo extends HiveObject

Fill two properties and you’re done

leading: res.complete
? Icon(Icons.check_box)
: Icon(Icons.check_box_outline_blank),
onTap: () {
res.complete = !res.complete;
res.save();
});

The link to the commit on the Github applications at this point.

Creating an application Step 3 — swipe left — delete

Here, too, everything is simple. We wrap the widget in which the case is stored in dismissable and again use the service removal method.

background: Container(color: Colors.red),
key: Key(res.id),
onDismissed: (direction) {
res.delete();
},

That’s all, we got a fully working application that stores data in a local database.

Runnable app code on the Github — https://github.com/awaik/todo_hive_example

--

--