Using Hive database the right way in Flutter - 2022

Introduction

For practically all apps, local data persistence is going to be necessary in one way or another. We should have a mechanism to store our user’s data locally, it could be a shopping cart from a previous session, a selected theme of the app, and the user’s credentials. The management and storage of data are essential elements of app development, and Flutter apps are no different.

Here is where Hive comes in.

What is Hive?

Hive is a high-performant, with no SQL, fast key-value database written in pure Dart. This makes it cross-platform, as it can run on all platforms that support the Dart programming language. On top of that, it is encrypted with AES-256.

What are the advantages of Hive?

  • It supports all platforms (mobile, desktop, and web).
  • It is very similar to the Dart map. (It uses key-value).
  • Less and clear code to write.
  • Easy to use.
  • Highly flexible.
  • No need for prior knowledge about relational databases.
  • No native dependencies.
  • It can store a large amount of data as long as it falls into supported primitives(string, number, list, map, etc) and Dart objects.

Here is a graph that benchmarks Hive against other similar database solutions:

Screenshot from 2022-11-21 08-41-40

It is very amusing how Hive beats almost all of them.

Enough theory, Let us dive into the fun part, starting from installing Hive to making CRUD operations. In this example, we are going to build a simple history app that uses Hive for local persistence.

This is the app we are going to build by the end of this article.

1. Installing Hive to our history project

Add the Hive and hive_flutter packages to our pubspec.yaml file:

dependencies:
  hive: ^[latest version]
  hive_flutter: ^[latest version]
  intl: ^[latest version]

dev_dependencies:
  hive_generator: ^[latest version]
  build_runner: ^[latest version]

we have added intl in our package to format our DateTime.

2. Initializing Hive

Let’s go to our main.dart file and add this before calling runApp():

// make the main() async and await for Hive initialization to finish.
void main() async {
  // Initializing Hive
  await Hive.initFlutter();
  runApp(MyApp());
}

3. creating our History model class

We need to create our History model class that has the name, createdDate, and price of the shopped product.

Note: Do not make your variables final or else it will throw you an error when you try to update the value later since final variables are set only once.

class History{
  late  String name;
  late  DateTime createdDate;
  late  double price;

  History({
    required this.name,
    required this.createdDate,
    required this.price,
  });
}

Using TypeAdapter

All primitive types, including “List,” “Map,” “DateTime,” and “Uint8List,” are generally supported by Hive. However, there are situations when we may need to store custom model classes that facilitate data administration.

We may accomplish this by utilizing a TypeAdapter, which creates the “to” and “from” binary methods.TypeAdapter helps to avoid any errors that could happen while writing by hand (and also because it is quicker).

Generating the Hive adapter

Annotate a class with the @HiveType annotation and a typeId to create a TypeAdapter for it (between 0 and 223). Don’t forget to put @HiveField annotations on all fields that need to be saved.

We will use the dev_dependencies we have already added in our pubspec.yaml file to generate the TypeAdapter for Hive.

Now, let’s annotate the class with @HiveType(typeId: 0) and @HiveField(id) for the attributes.

import 'package:hive/hive.dart';

part 'history_model.g.dart';

@HiveType(typeId: 0)
class History extends HiveObject {
  @HiveField(0)
  late String name;
  @HiveField(1)
  late DateTime createdDate;
  @HiveField(2)
  late double price;

  History({
    required this.name,
    required this.createdDate,
    required this.price,
  });
}


As you can see we have extended our History class to the HiveObject class which gives us convenient methods we can use on our ‘History’ object. For instance, we can use save() and delete() easily.
It has more great features. You can go ahead and check HiveObject out for yourself.

We can now generate the code using the following command using our terminal:

flutter packages pub run build_runner build

After a few seconds, the above command will generate a file named ‘history_model.g.dart’.

Note:

Do not try to modify the generated code at all.

Registering the generated TypeAdapter to our main.dart

Before opening the box that uses the TypeAdapter, we should register inside the “main()” method because failing to do so would result in an exception.

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();

  await Hive.initFlutter();

  Hive.registerAdapter(HistoryAdapter());

  runApp(const MyApp());
}

4. Opening a box

Let’s start by defining a hive box.
Hive employs the idea of “boxes” to organize data into databases. On a SQL database, a box is comparable to a table, with the exception that boxes don’t have a rigid structure. This indicates that boxes are adaptable and can only manage simple data interactions.

We need to open the box to get the info that is within. This transfers the complete contents of the box from local storage into the memory, making it possible to quickly access any data that may be inside.

// make the main() async and await for Hive to finish opening the box.
Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();

  await Hive.initFlutter();

  Hive.registerAdapter(HistoryAdapter());

  await Hive.openBox<History>('history');

  runApp(const MyApp());
}

We have done most of the work now, and we’re good to perform database operations.

We need to create our ‘HistoryScreen’ page which is going to help as the home page to display the list of history we have added.

For now, the HistoryScreen is just a simple Stateful class that has an AppBar and Container as its body.

class HistoryScreen extends StatefulWidget {
  const HistoryScreen({super.key});

  @override
  _HistoryScreenState createState() => _HistoryScreenState();
}

class _HistoryScreenState extends State<HistoryScreen> {

  @override
  Widget build(BuildContext context) => Scaffold(
        appBar: AppBar(
          title: const Text('Hive History Tracker'),
          centerTitle: true,
        ),
        body: Container(),
      );
}

Inside our dispose() method we have called Hive.close(); to close opened boxes and free our app.

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

If you ever want to close only one box, you can write Hive.box('boxName').close(); instead of using Hive.close(); which will end up closing all opened boxes.

For more simplicity and clarity we are going to create a class that returns a history box that has been opened before in our main(); method.


// History is our model class
class Boxes {
  static Box<History> getHistories() => Hive.box<History>('historyBox');
}

##5. Let’s perform CRUD

Create operation

we are going to call Boxes.getHistories(); to get the opened box. After that, we are going to use the ‘box.add()’ method to add our new history to the box.


  Future addHistory(String name, double price) async {

  final history = History(price: price, createdDate: DateTime.now(), name: name);

    final box = Boxes.getHistories();

    box.add(history);
  }

When we use the add() method, it saves the value with an auto-increment key.

if you ever want to control the key, you can use the put() which takes a key and value.
For example, we could add our new history as such.


  Future addHistory(String name, double price) async {

    final history = History(price: price, createdDate: DateTime.now(), name: name);

    final box = Boxes.getHistories();

    box.put('historyKey', history);

  }

Retrieve operation

we can retrieve our data from our Hive box using the method get();. We just have to
provide the key for retrieving its value.


 Future getHistory() async {

    final box = Boxes.getHistories();

    box.get('historyKey');

  }

To get all the keys and values from the box:


 Future getHistory() async {

    final box = Boxes.getHistories();

    // This will return all the keys stored in the box
    box.keys;

    // This will return all the keys stored in the box
    box.values;
  }

Update operation

To edit or update the data of a particular key, we use the save() method. The save method comes from the HiveObject class that we have extended when creating our `History’ model class.

note: You need to extend to HiveObject if you want to use the save() method.


  // we are passing the new name and price value with the current history to be updated.
  void updateHistory(History history, String name, double price) {
    history.name = name;

    history.price = price;

    history.save();
  }



In cases where you used the put(); method when storing your data, you are going to need to pass the key you used for that particular data with the new value.

...

final box = Boxes.getHistories();

get.put('hitoryKey', newHistoryValue);

Delete operation

The simplest way to delete a particular data is using the delete() method that comes from the HiveObject class that we have extended when creating our `History’ model class.

note: You need to extend to HiveObject if you want to use the delete() method.


  // We need to pass the history that we want to delete.
  void deleteHistory(History history) {
    history.delete();
  }

But if you want to take control of the deletion of the data, you may use the other option which is using the ‘delete()’ method from the HIve class itself passing the key for that specific data.

 void deleteHistory()  {

    final box = Boxes.getHistories();

   box.delete('historyKey');
  }

The Rest of the UI part

Let’s add the rest of the user interfaces in our history app.

1. HistoryScreen

We are going to replace the Container with

       ValueListenableBuilder<Box<History>>(
          valueListenable: Boxes.getHistories().listenable(),
          builder: (context, box, _) {
            final histories = box.values.toList().cast<History>();

            return buildContent(histories);
          },
        ), 

We are using the ValueListenableBuilder widget will automatically register itself as a listener of the ValueListenable and call the builder with updated values when the value changes.

The ValueListenable is going to listen to our Boxes.getHistories().listenable() and for this, to work we need to import hive_flutter as such.

import 'package:hive_flutter/hive_flutter.dart';

The buildContent() is going to be a custom widget method that will return a list of history contents.


  Widget buildContent(List<History> histories) {
    if (histories.isEmpty) {
      return const Center(
        child: Text(
          'No history yet!',
          style: TextStyle(fontSize: 24),
        ),
      );
    } else {
      return Column(
        children: [
          const SizedBox(height: 24),
          const Text(
            'History',
            style: TextStyle(
              fontWeight: FontWeight.bold,
              fontSize: 20,
            ),
          ),
          const SizedBox(height: 24),
          Expanded(
            child: ListView.builder(
              padding: const EdgeInsets.all(8),
              itemCount: histories.length,
              itemBuilder: (BuildContext context, int index) {
                final history = histories[index];
                return buildHistory(context, history);
              },
            ),
          ),
        ],
      );
    }
  }

We have a FloatingActionButton that will call HistoryDialog which will be used to add a new history.


        floatingActionButton: FloatingActionButton(
          child: const Icon(Icons.add),
          onPressed: () => showDialog(
            context: context,
            builder: (context) => HistoryDialog(
              onClickedDone: addHistory,
            ),
          ),
        ),

home

We are going to build a custom ListTile to show every History data.
We can use the intl package that’s added in our pubspec.yaml to format the DateTime.

When we long-press the ListTile, it will take us to update that particular History data.
When we press on the trailing of the ListTile, it deletes that ‘History’ data.

 Widget buildHistory(BuildContext context, History history) {
    final date = DateFormat.yMMMd().format(history.createdDate);
    final price = '\$${history.price.toStringAsFixed(2)}';

    return Card(
      color: Colors.white,
      child: ListTile(
        onLongPress: () => Navigator.of(context).push(
          MaterialPageRoute(
            builder: (context) => HistoryDialog(
              history: history,
              onClickedDone: (name, price) => updateHistory(
                history,
                name,
                price,
              ),
            ),
          ),
        ),
        leading: CircleAvatar(
          backgroundColor: Colors.blue,
          child: Text(
            history.price.toStringAsFixed(2),
            style: const TextStyle(color: Colors.white),
          ),
        ),
        title: Text(
          history.name,
          maxLines: 2,
          style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
        ),
        subtitle: Text(date),
        trailing: IconButton(
          icon: const Icon(
            Icons.delete,
          ),
          onPressed: () => deleteHistory(history),
          color: Colors.red,
        ),
      ),
    );
  }


Finally, our HistoryScreen is going to look like this including all of the above methods and classes we have gone through.


class HistoryScreen extends StatefulWidget {
  const HistoryScreen({super.key});

  @override
  _HistoryScreenState createState() => _HistoryScreenState();
}

class _HistoryScreenState extends State<HistoryScreen> {
  @override
  void dispose() {
    Hive.close();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) => Scaffold(
        appBar: AppBar(
          title: const Text('Hive History Tracker'),
          centerTitle: true,
        ),
        body: ValueListenableBuilder<Box<History>>(
          valueListenable: Boxes.getHistories().listenable(),
          builder: (context, box, _) {
            final histories = box.values.toList().cast<History>();

            return buildContent(histories);
          },
        ),
        floatingActionButton: FloatingActionButton(
          child: const Icon(Icons.add),
          onPressed: () => showDialog(
            context: context,
            builder: (context) => HistoryDialog(
              onClickedDone: addHistory,
            ),
          ),
        ),
      );

  Widget buildContent(List<History> histories) {
    if (histories.isEmpty) {
      return const Center(
        child: Text(
          'No history yet!',
          style: TextStyle(fontSize: 24),
        ),
      );
    } else {
      return Column(
        children: [
          const SizedBox(height: 24),
          const Text(
            'History',
            style: TextStyle(
              fontWeight: FontWeight.bold,
              fontSize: 20,
            ),
          ),
          const SizedBox(height: 24),
          Expanded(
            child: ListView.builder(
              padding: const EdgeInsets.all(8),
              itemCount: histories.length,
              itemBuilder: (BuildContext context, int index) {
                final history = histories[index];
                return buildHistory(context, history);
              },
            ),
          ),
        ],
      );
    }
  }

  Widget buildHistory(BuildContext context, History history) {
    final date = DateFormat.yMMMd().format(history.createdDate);
    final price = '\$${history.price.toStringAsFixed(2)}';

    return Card(
      color: Colors.white,
      child: ListTile(
        onLongPress: () => Navigator.of(context).push(
          MaterialPageRoute(
            builder: (context) => HistoryDialog(
              history: history,
              onClickedDone: (name, price) => updateHistory(
                history,
                name,
                price,
              ),
            ),
          ),
        ),
        leading: CircleAvatar(
          backgroundColor: Colors.blue,
          child: Text(
            history.price.toStringAsFixed(2),
            style: const TextStyle(color: Colors.white),
          ),
        ),
        title: Text(
          history.name,
          maxLines: 2,
          style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
        ),
        subtitle: Text(date),
        trailing: IconButton(
          icon: const Icon(
            Icons.delete,
          ),
          onPressed: () => deleteHistory(history),
          color: Colors.red,
        ),
      ),
    );
  }

  Future addHistory(String name, double price) async {
    final history =
        History(price: price, createdDate: DateTime.now(), name: name);

    final box = Boxes.getHistories();

    box.add(history);
  }

  void updateHistory(History history, String name, double price) {
    history.name = name;
    history.price = price;
    history.save();
  }

  void deleteHistory(History history) {
    history.delete();
  }
}


2. HistoryDialog screen

The historyDialog is going to be used to add and update our History data. It takes onClicked callback function and a History data if the operation is to update a particular History data.


import 'package:flutter/material.dart';
import 'package:hive_example/model/history_model.dart';

class HistoryDialog extends StatefulWidget {
  final History? history;
  final Function(String name, double price) onClickedDone;

  const HistoryDialog({
    Key? key,
    this.history,
    required this.onClickedDone,
  }) : super(key: key);

  @override
  _HistoryDialogState createState() => _HistoryDialogState();
}

class _HistoryDialogState extends State<HistoryDialog> {
  final formKey = GlobalKey<FormState>();
  final nameController = TextEditingController();
  final priceController = TextEditingController();

  @override
  void initState() {
    super.initState();

    if (widget.history != null) {
      final history = widget.history!;
      nameController.text = history.name;
      priceController.text = history.price.toString();
    }
  }

  @override
  void dispose() {
    nameController.dispose();
    priceController.dispose();

    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final isEditing = widget.history != null;
    final title = isEditing ? 'Edit History' : 'Add History';

    return AlertDialog(
      title: Text(title),
      content: Form(
        key: formKey,
        child: SingleChildScrollView(
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: <Widget>[
              const SizedBox(height: 8),
              buildName(),
              const SizedBox(height: 8),
              buildPrice(),
            ],
          ),
        ),
      ),
      actions: <Widget>[
        buildCancelButton(context),
        buildAddButton(context, isEditing: isEditing),
      ],
    );
  }

  Widget buildName() => TextFormField(
        controller: nameController,
        decoration: const InputDecoration(
          border: OutlineInputBorder(),
          hintText: 'Enter Name',
        ),
        validator: (name) =>
            name != null && name.isEmpty ? 'Enter a name' : null,
      );

  Widget buildPrice() => TextFormField(
        decoration: const InputDecoration(
          border: OutlineInputBorder(),
          hintText: 'Enter Price',
        ),
        keyboardType: TextInputType.number,
        validator: (price) => price != null && double.tryParse(price) == null
            ? 'Enter a valid number'
            : null,
        controller: priceController,
      );

  Widget buildCancelButton(BuildContext context) => TextButton(
        child: const Text('Cancel'),
        onPressed: () => Navigator.of(context).pop(),
      );

  Widget buildAddButton(BuildContext context, {required bool isEditing}) {
    final text = isEditing ? 'Save' : 'Add';

    return TextButton(
      child: Text(text),
      onPressed: () async {
        final isValid = formKey.currentState!.validate();

        if (isValid) {
          final name = nameController.text;
          final price = priceController.text;

          widget.onClickedDone(name, double.tryParse(price)!);

          Navigator.of(context).pop();
        }
      },
    );
  }
}


Add screenshot

add

update history screenshot
update

we have successfully built our History app using Hive as the local persistent database.

home 2

Note: You can get the source code using the GitHub link for free.

https://github.com/mekonnen070/hive_example
[/quote]

mple
[/quote]