11

Hive (Flutter Tutorial) – Lightweight & Fast NoSQL Database

Storing data locally is a task which has to be done by almost every app. Maybe, you want to cache responses from a REST API or you're building an offline-only app. In any case, choosing the right local database can make all the difference in how quickly you can develop the app and also in how performant the app will be.

Hive is a lightweight, yet powerful database which is easy to develop with and it also runs fast on the device. Unless you absolutely need to model your data with many relationships, in which case you should probably use SQLite, choosing this pure-Dart package with no native dependencies (it runs on Flutter Web!) can be the best option.

Project Setup

In this tutorial, you're going to learn Hive by building a simple "contacts" app which will store the name and age of a person. Doing this will allow us to cover all the core concepts of Hive. The starter project contains some basic UI and also a Contact class having the two aforementioned fields.

Apart from the core hive package, there are also a bunch of supporting ones such as hive_flutter and also hive_generator which is used for creating custom TypeAdapters. Let's add all of them.

pubspec.yaml

dependencies:
  flutter:
    sdk: flutter
  hive: ^1.0.0
  hive_flutter: ^0.2.1
  # For OS-specific directory paths
  path_provider: ^1.3.0

dev_dependencies:
  hive_generator: ^0.5.1
  build_runner:

Diving into Hive

Hive is centered around the idea of boxes, and no, they don't contain bees 🐝😉. A Box has to be opened before use. In addition to the plain-flavored Boxes, there are also options which support lazy-loading of values and encryption. Basically, Hive is a persistent Map on steroids.

Initialization

Before performing any of the CRUD operations, Hive needs to be initialized to, among other things, know in which directory it stores the data. It's best to initialize Hive right in the main method.

main.dart

void main() async {
  final appDocumentDir = await path_provider.getApplicationDocumentsDirectory();
  Hive.init(appDocumentDir.path);
  runApp(MyApp());
}

Boxes

Data can be stored and read only from an opened Box. Opening a Box loads all of its data from the local storage into memory for immediate access.

final contactsBox = await Hive.openBox('contacts');

To get an already opened instance, you can call Hive.box('name') instead. It doesn't matter though if you try to call openBox multiple times. Hive is smart, and it will return an already opened box with the given name, if you've previously called that method.

If you have a lot of data inside a Box and you'd rather not load it all into memory for even faster access, use a LazyBox. It's still fast, but reading values from it happens right from the uncached local storage.

To keep the code clean, it's probably a wise idea to open the Box from only a single place and then to get it using Hive.box('name'). Also, to prevent holding unnecessary data in memory, you can close the Box when you're not going to need it anymore. Hive also has a handy method to close all boxes. It's a good practice to do this before the app exits, although as per the official documentation, it's not really necessary to do so.

Before your application exits, you should call Hive.close() to close all open boxes. Don’t worry if the app is killed before you close Hive, it doesn’t matter.

main.dart

class _MyAppState extends State<MyApp> {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Hive Tutorial',
      home: FutureBuilder(
        future: Hive.openBox('contacts'),
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.done) {
            if (snapshot.hasError)
              return Text(snapshot.error.toString());
            else
              return ContactPage();
          }
          // Although opening a Box takes a very short time,
          // we still need to return something before the Future completes.
          else
            return Scaffold();
        },
      ),
    );
  }

  @override
  void dispose() {
    Hive.close();
    super.dispose();
  }
}

Storing Data

With the Box opened, let's add a new contact to the database after we submit the form. There are two basic options of adding data - either call put(key, value) and specify the key yourself, or call add and utilize Hive's auto-incrementing keys. Unless you absolutely need to define the keys manually, calling add is the better and simpler option.

Keys must be either of type String or int.

The problem is that Hive supports only primitive types like int or String, plus additional standard types, which are List, Map and DateTime. Of course, we have our own custom Contact model class which we'd like to utilize.

contact.dart

class Contact {
  final String name;
  final int age;

  Contact(this.name, this.age);
}

Trying to call the following would result in an exception.

contactsBox.add(Contact('John Doe', 20));

Out of the box (😉), Hive doesn't know how to store objects of type Contact. Sure, we could just convert the objects to JSON strings and call it a day, but there is a better, more native solution, and that is adding a TypeAdapter.

Creating a TypeAdapter

Behind the scenes, Hive works with binary data. While it's entirely possible to write a custom adapter which fumbles with a ​​BinaryWriter and a BinaryReader, it's much easier to let the hive_generator package do the hard job for you. Making an adapter for the Contact class is then as simple as adding a few annotations.

contact.dart

import 'package:hive/hive.dart';

part 'contact.g.dart';

@HiveType()
class Contact {
  @HiveField(0)
  final String name;

  @HiveField(1)
  final int age;

  Contact(this.name, this.age);
}

This, of course, requires running the Flutter developer's most favorite command:

flutter packages pub run build_runner build
There are some precautions you should take when updating a class with a generated TypeAdapter. For example the number of a particular field should not be changed. Learn more from the official docs.

Just generating a TypeAdapter is not enough though. We also have to register it. There are two options in how this can be done.

  1. Register the TypeAdapter just for a single Box.
  2. Register it for all the Boxes.

In the case of our Contact App, we have only one Box either way, so we're going to register the TypeAdapter globally.

main.dart

void main() async {
  final appDocumentDir = await path_provider.getApplicationDocumentsDirectory();
  Hive.init(appDocumentDir.path);
  Hive.registerAdapter(ContactAdapter(), 0);
  runApp(MyApp());
}

Adding a Contact

Putting this all together in the Contact App ​ we're building, we can now add contacts inputted from the form to the database.​​​​

new_contact_form.dart

...
void addContact(Contact contact) {
  final contactsBox = Hive.box('contacts');
  contactsBox.add(contact);
}
...
RaisedButton(
  child: Text('Add New Contact'),
  onPressed: () {
    _formKey.currentState.save();
    final newContact = Contact(_name, int.parse(_age));
    addContact(newContact);
  },
)
...

Reading Contacts

We want to display all the contacts inside a ListView, so we somehow need to access all of the contacts present inside the Box. Firstly, we'll need to specify the itemCount for the ListView.builder

contact_page.dart

ListView _buildListView() {
  final contactsBox = Hive.box('contacts');

  return ListView.builder(
    itemCount: contactsBox.length,
    itemBuilder: (BuildContext context, int index) {
      // Show contacts
    },
  );
}

Now comes the time to display the contacts on the screen. The simplest way to retrieve data is to call the contactsBox.get(someKey) method. Since we're using auto-incrementing keys, we should be simply able to use the index parameter.

contact_page.dart

itemBuilder: (BuildContext context, int index) {
  final contact = contactsBox.get(index) as Contact;

  return ListTile(
    title: Text(contact.name),
    subtitle: Text(contact.age.toString()),
  );
},

It works, of course, only after you rebuild the widget after adding a new contact. We're going to fix that next. But first, although the get method works with the data we currently have, is it always a safe bet to use it from things like ListView builders? What if you call box.put() instead of add() and therefore specify the keys yourself. How would you display such "custom-keyed" entries in a ListView?

Keys vs Indexes

In addition to accessing stored values by keys, you can also access them by an index. In this regard, Hive works very much like a regular List. Every new value has practically an auto-incremented index. Of course, this means that by using auto-incrementing keys, the values of the two will be "in sync".

However, as soon as store a value by calling box.put('customKey', value), or when a value somewhere in the middle of the "list" is deleted, this implicit synchronization of keys and indexes will be gone. Therefore, in a ListView and in other places where you don't really get values by their keys, you should call box.getAt() instead of get(), which takes in an index instead of a key.

final contact = contactsBox.getAt(index) as Contact;

Watching for Changes

Having to manually rebuild the UI every time a value changes inside a Box is not the best developer experience. That's why there is the box.watch() method which returns a Stream of BoxEvents. This is plenty enough if you have a proper state management, for example with Bloc, where you don't expose Boxes directly to the UI. With a simple state management though, there's a better solution to watching the values than to plug this Stream into a vanilla StreamBuilder.

WatchBoxBuilder Widget

While the core hive package can run on just about any Dart platform, hive_flutter adds a WatchBoxBuilder widget to simplify the UI development a bit by not having to use the StreamBuilder together with all its boilerplate. Now, we can effortlessly update the UI whenever any change happens inside the contactsBox.

contact_page.dart

Widget _buildListView() {
return WatchBoxBuilder(
  box: Hive.box('contacts'),
  builder: (context, contactsBox) {
    return ListView.builder(
      itemCount: contactsBox.length,
      itemBuilder: (BuildContext context, int index) {
        final contact = contactsBox.getAt(index) as Contact;

        return ListTile(
          title: Text(contact.name),
          subtitle: Text(contact.age.toString()),
        );
      },
    );
  },
);

Updating & Deleting Contacts

Updating a value happens by overriding an old one either with the put(key) or putAt(index) methods. For deleting, there is, of course, delete or deleteAt.

We're going to perform these last two of CRUD operations from two IconButtons to keep the code simple. All of the updates and deletes will be automatically reflected in the UI because of the WatchBoxBuilder widget.

contact_page.dart

return ListTile(
  title: Text(contact.name),
  subtitle: Text(contact.age.toString()),
  trailing: Row(
    mainAxisSize: MainAxisSize.min,
    children: <Widget>[
      IconButton(
        icon: Icon(Icons.refresh),
        onPressed: () {
          return contactsBox.putAt(
            index,
            Contact('${contact.name}*', contact.age + 1),
          );
        },
      ),
      IconButton(
        icon: Icon(Icons.delete),
        onPressed: () => contactsBox.deleteAt(index),
      ),
    ],
  ),
);

Box Compaction

According to the official documentation:

Hive is an append-only data store. When you change or delete a value, the change is written to the end of the box file. This leads sooner or later to a growing box file. Hive may automatically “compact” your box at any time.

Since we are both updating and deleting values, sooner or later, the compaction will kick in. While you can leave the decision of when to compact completely up to Hive, invoking compaction manually is also possible, although rarely needed. We could, however, call compact() right before closing all the Boxes, for example.

main.dart

@override
void dispose() {
  Hive.box('contacts').compact();
  Hive.close();
  super.dispose();
}

Another option is to provide a custom compactionStrategy while opening a Box.

main.dart

future: Hive.openBox(
  'contacts',
  compactionStrategy: (int total, int deleted) {
    return deleted > 20;
  },
),

The default compaction strategy is reasonable enough though, so in most cases, you can just ignore what you learned in this last section altogether.

Conclusion

Hive is an easy-to-use, yet fast database with a support for custom TypeAdapters. Being completely platform independent is also a huge plus. As of writing this, the author of this amazing package, Simon Leier, is working on adding the support for queries. Once that's implemented, Hive will be an even more powerful, fully-featured database. Subscribe below to grow your Flutter coding skills by getting important Flutter news sent right into your inbox on a weekly basis.

Matej Rešetár
 

Matej is an app developer with a knack for teaching others. If he's not programming, making tutorials or doing other business, he's mostly working out, listening to audiobooks and taking cold showers.

  • anan says:

    final appDocumentDir = await path_provider.getApplicationDocumentsDirectory();
    i got error on it.

  • anan says:

    the error looks like this
    E/flutter (27356): [ERROR:flutter/lib/ui/ui_dart_state.cc(148)] Unhandled Exception: ServicesBinding.defaultBinaryMessenger was accessed before the binding was initialized.
    E/flutter (27356): If you’re running an application and need to access the binary messenger before runApp() has been called (for example, during plugin initialization), then you need to explicitly call the WidgetsFlutterBinding.ensureInitialized() first.
    E/flutter (27356): If you’re running a test, you can call the TestWidgetsFlutterBinding.ensureInitialized() as the first line in your test’s main() method to initialize the binding.
    E/flutter (27356): #0 defaultBinaryMessenger.
    package:flutter/…/services/binary_messenger.dart:73
    E/flutter (27356): #1 defaultBinaryMessenger
    package:flutter/…/services/binary_messenger.dart:86
    E/flutter (27356): #2 MethodChannel.binaryMessenger
    package:flutter/…/services/platform_channel.dart:140

    • Did you stop and start the app completely?

      • anan says:

        I Made new project,

        change this function :

        void main() async{
        final appDocumentDir = await path_provider.getApplicationDocumentsDirectory();
        Hive.init(appDocumentDir.path);
        runApp(MyApp());
        }

        add dependencies:

        dependencies:
        flutter:
        sdk: flutter

        path_provider: ^1.3.0
        hive: ^1.0.0
        hive_flutter: ^0.2.1

        then run the project for the first time. it still got this error. the screen of emulator still white and stuck in white screen without any widget loaded.

        will you check this ?

        Launching libmain.dart on Android SDK built for x86 in debug mode…
        Built buildappoutputsapkdebugapp-debug.apk.
        I/flutter (32192): Overflow on channel: flutter/lifecycle. Messages on this channel are being discarded in FIFO fashion. The engine may not be running or you need to adjust the buffer size if of the channel.
        I/Choreographer(32192): Skipped 84 frames! The application may be doing too much work on its main thread.
        D/EGL_emulation(32192): eglMakeCurrent: 0xe9f857e0: ver 2 0 (tinfo 0xd41fedb0)
        I/OpenGLRenderer(32192): Davey! duration=1617ms; Flags=1, IntendedVsync=82000941874057, Vsync=82002341874001, OldestInputEvent=9223372036854775807, NewestInputEvent=0, HandleInputStart=82002345063660, AnimationStart=82002345203460, PerformTraversalsStart=82002348638560, DrawStart=82002371170960, SyncQueued=82002374936060, SyncStart=82002377670460, IssueDrawCommandsStart=82002380969460, SwapBuffers=82002466093860, FrameCompleted=82002562433860, DequeueBufferDuration=25011000, QueueBufferDuration=349000,
        E/flutter (32192): [ERROR:flutter/lib/ui/ui_dart_state.cc(148)] Unhandled Exception: ServicesBinding.defaultBinaryMessenger was accessed before the binding was initialized.
        E/flutter (32192): If you’re running an application and need to access the binary messenger before runApp() has been called (for example, during plugin initialization), then you need to explicitly call the WidgetsFlutterBinding.ensureInitialized() first.
        E/flutter (32192): If you’re running a test, you can call the TestWidgetsFlutterBinding.ensureInitialized() as the first line in your test’s main() method to initialize the binding.
        E/flutter (32192): #0 defaultBinaryMessenger.
        package:flutter/…/services/binary_messenger.dart:73
        E/flutter (32192): #1 defaultBinaryMessenger
        package:flutter/…/services/binary_messenger.dart:86
        E/flutter (32192): #2 MethodChannel.binaryMessenger
        package:flutter/…/services/platform_channel.dart:140
        E/flutter (32192): #3 MethodChannel.invokeMethod
        package:flutter/…/services/platform_channel.dart:314
        E/flutter (32192):
        E/flutter (32192): #4 getApplicationDocumentsDirectory
        package:path_provider/path_provider.dart:84
        E/flutter (32192):
        E/flutter (32192): #5 main
        package:hive_anan/main.dart:6
        E/flutter (32192):
        E/flutter (32192): #6 _runMainZoned.. (dart:ui/hooks.dart:239:25)
        E/flutter (32192): #7 _rootRun (dart:async/zone.dart:1124:13)
        E/flutter (32192): #8 _CustomZone.run (dart:async/zone.dart:1021:19)
        E/flutter (32192): #9 _runZoned (dart:async/zone.dart:1516:10)
        E/flutter (32192): #10 runZoned (dart:async/zone.dart:1500:12)
        E/flutter (32192): #11 _runMainZoned. (dart:ui/hooks.dart:231:5)
        E/flutter (32192): #12 _startIsolate. (dart:isolate-patch/isolate_patch.dart:305:19)
        E/flutter (32192): #13 _RawReceivePortImpl._handleMessage (dart:isolate-patch/isolate_patch.dart:172:12)
        E/flutter (32192):

  • anan says:

    i got the problem for my error

    void main() async{
    WidgetsFlutterBinding.ensureInitialized();
    var appDocumentDir = await path_provider.getApplicationDocumentsDirectory();
    Hive.init(appDocumentDir.path);
    runApp(MyApp());

    }

    add this WidgetsFlutterBinding.ensureInitialized();
    before you get the path

  • >