Sitemap
Bhavesh Mankar

Bhavesh Mankar

Building Cenlo | Helping professionals automate time tracking with absolute privacy

Press enter or click to view image in full size
https://unsplash.com/photos/a-bunch-of-bees-that-are-on-a-beehive-DIwC450lRGI

Local Persistant Storage in Flutter : Getting Started with HiveDB

9 min readOct 2, 2024

--

Store data with local persistance in flutter apps with the help of hive database.

Overview

I have used HiveDB for local persistant storage in many of my apps and it is one of my favorite storage soutions when it comes to ease of use. I recently noticed that the docs for HiveDB are down and are not accessible. The pacakge is not maintained anymore and was last updated 2 years ago(as of Oct 2024), yet has a popularity score of 100% and is one of the most used databases for local persistance storage.

Since the docs are down I thought of writing a Getting Started guide for HiveDB.

We will discuss about the following in this article

  1. What is Hive Database?
  2. Getting Started with Hive
  3. Basic Hive Concepts
  4. Generating TypeAdapters
  5. Creating a simple Todo List app
  6. Conclusion

What is Hive Database?

Hive is a lightweight, efficient NoSQL database specifically designed for local persistent storage for Flutter applications. It is a high performant database with a key-value storage model. This makes it appropriate for use in scenerios were quick data retrival is crucial. Hive allows creation of someting called as TypeAdapters which allow storing any type object, in addition to the primitive data types like List, Map, DateTime, BigInt and Uint8List that hive supports out of the box.

Getting Started with Hive

In order to use hive we need to add the following dependencies to the pubspec.yaml file

dependencies:
analyzer: ^4.7.0
flutter:
sdk: flutter
#hiveDB related packages
hive: ^2.2.3
hive_flutter: ^1.1.0

dev_dependencies:
flutter_test:
sdk: flutter
#package to generate dart code
build_runner: ^2.4.8
#hive generator for generating type adapters
hive_generator: ^1.1.0

Let’s first go through the dependencies

  1. hive: The package required to use hive database
  2. hive_flutter: This package makes it easier to use hive in flutter apps.
  3. build_runner: This package is used to generate dart code.
  4. hive_generator: This package is used to generate TypeAdapters which can be stored in hive boxes.

Next, we need to initialize hive in the main function

import 'package:flutter/material.dart';
import 'package:hive_flutter/hive_flutter.dart';

void main() async {
//Initialize Hive
await Hive.initFlutter();
runApp(const MainApp());
}

This will initialize hive and will store the hive box in a valid directory which changes based on the operating system. By default, Hive is initialized in the application documents directory. We can optionally specify the directory in which Hive should be initialized.

await Hive.initFlutter('path_to_directory');

This is all the setup needed in order to start using hive. Just add all the required dependencies and use await Hive.initFlutter(); to initialize hive, that’s it.

Basic Hive Concepts

In order to work with hive we need to know about some basic hive concepts.

Hive Boxes

Hive stores data inside boxes in the form of key-value pair. We first need to open a hive box before storing any data. This can be done by using the openBox command

var todosBox = await Hive.openBox('todos');

This will create a hive box where we can store our data. If we navigate to the directory where we have initilized Hive we will notice a new file by the name of todos.hive. For example, if a windows application is launched, then the box files by the extension of .hive can be found in Documents dir. This is the file which will store all the key-value pairs.

Operations on Hive Boxes

We can easily perform CRUD(Create, Read, Update, Delete) operations on Hive Boxes.

  //Initialize Hive
await Hive.initFlutter();

// Open the box after initializing Hive
var todosBox = await Hive.openBox('todos');

//CREATE
// Store title using put method by specifying the key and value to store
todosBox.put('title', 'Football Match');
debugPrint('Length: ${todosBox.length}');

//READ
// Retrive the title using get method by specifying the key
var title = todosBox.get('title');
debugPrint('Title : $title');

//UPDATE
// Update the title using the put method by specifying the key
todosBox.put('title', 'Grocery Shopping');
// Retrive the updated title using get by specifying the key
title = todosBox.get('title');
debugPrint('Updated title: $title');

//DELETE
// delete data using delete by specifying the key
todosBox.delete('title');
debugPrint('Length: ${todosBox.length}');

We have performed CRUD operations on todosBox using put(), get() and delete() methods. These methods all have a required parameter ‘key’ which is used to store and access data.

Get Bhavesh Mankar’s stories in your inbox

Join Medium for free to get updates from this writer.

Hive also supports auto incrementing keys which allows storing data without specifying ‘key’, instead hive automatically generates incrementing indices and provides methods like add(), getAt(), putAt() and deleteAt() for CRUD operations.

We can strore any data in hive as long as we are storing primitive data types, but that’s no fun. What if we want to store some custom data? This is were TypeAdapters come into paly.

Generating TypeAdapters

Hive allows storing custom objects with the help of TypeAdapters, but creating TypeAdapters requires boilerplate code. Fortunately, we can make use of build_runner package to generate TypeAdapters with the help of hive_generator.

In order to generate TypeAdapers we need to do some setup. Let’s look at todo_model.dart.

//FILE: todo_model.dart
import 'package:hive/hive.dart';

//specify name of the part file which will be generated by hive_generator
part 'todo_model.g.dart';

//Specify the HiveType with a type id between 0 and 233
//NOTE: No two TypeAdapters can have the same typeId
@HiveType(typeId: 0)
class Todo {
//Annotate class members with HiveField with each
//HiveField having incrementing index
@HiveField(0)
String title;
@HiveField(1)
String description;

//Default value can be specified if needed
@HiveField(3, defaultValue: false)
bool isDone;

Todo({
required this.title,
required this.description,
this.isDone = false,
});
}

Now, let’s breakdown the structure of todo_model.dart

First: We have specified the name that the generated file will have as todo_model.g.dart here ‘g’ specifies that it is a generated file.

Second: We have annotated the class with HiveType which takes an index. Every single class annotated with HiveType should have a unique index between 0 to 233

Third: We have annotated the class members with HiveField each having a incrementing index. Also we have specified defaultValue for isDone so if no value is provided isDone will default to false. HiveField can have a range of 0 to 255

That’s is thats all the code that we need for generating a TypeAdapter . Now, in order to generate a TypeAdapter type the below command in terminal window.

dart run build_runner build

After executing the command a file named todo_model.g.dart will be generated in the same folder as todo_model.dart . This file will contain the code generated by hive_generator package.

The file will have a class named TodoAdapter extending TypeAdapter<Todo> and all the logic that is required to convert data from and to binary. We do not need to know any more information about this file.

Next Up, in order to use the TypeAdapter, we first need to register it.

void main() async {
//Initialize Hive
await Hive.initFlutter();
//Register a TypeAdapter
Hive.registerAdapter(TodoAdapter());
}

Now, that we have learned about all basic concepts of Hive and can kick things off by looking at a Todo List app.

Creating a simple Todo List app

The Todo List app will have a HomePage widget with a ListView and a FloatingActionButton.

  1. The initStatemethod creates a box called as todosBoxand calls loadTodos.The loadTodos function fetches the list of todos stored in the todosBox. After fetching the Todo items from the box it parses it as list and stores in todosList
  2. If the todosList is not empty then List View will return ListTiles. Each ListTile will have its
  • title as title of the Todo,
  • subtitle as the description of Todo and a
  • Checkbox as the traling widget which will track isDone property of Todo
  • IconButton as leading widget for deleting the Todo

3. On pressing the FloatingActionButton a SimpleDialog will be displayed with two TextFields for title and description of the Todo. Pressing ADD will add the Todo to the todosBox.

4. CRUD operations are handelled by these function functions

  • addTodo(): The addTodo function takes a Todo object as a parameter and adds it to todosBox. If the addition is successful, it updates the todosList by appending the newly created todo.
  • deleteTodo(): The deleteTodo function provides users with the ability to remove completed or unnecessary todo items from their lists. It accepts an index and attempts to delete the corresponding todo from the todosBox. If successful, it updates the todosList to remove the deleted item.
  • changeCompletionStatus(): The changeCompletionStatus accepts an index and a boolean value, this function enables users to toggle the completion status of a specific todo item. When called, it retrieves the todo from the Hive database, updates its isDone property according to the provided value, and saves the modified todo back into the todosBox. This function also updates the todosList to reflect the change.

Here is the complete code:

//FILE: todo_model.dart
import 'package:hive/hive.dart';

//specify name of the part file which will be generated by hive_generator
part 'todo_model.g.dart';

//Specify the HiveType with a type id between 0 and 233
//NOTE: No two TypeAdapters can have the same typeId
@HiveType(typeId: 0)
class Todo {
//Annotate class members with HiveField with each
//HiveField having incrementing index
@HiveField(0)
String title;
@HiveField(1)
String description;

//Default value can be specified if needed
@HiveField(3, defaultValue: false)
bool isDone;

Todo({
required this.title,
required this.description,
this.isDone = false,
});
}

///FILE: main.dart
/// The main function initializes Hive and runs the Flutter application.
import 'package:flutter/material.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:lib/todo_model.dart';
void main() async {
await Hive.initFlutter();
Hive.registerAdapter(TodoAdapter());
await Hive.openBox<Todo>('todos');

runApp(const MyApp());
}

class MyApp extends StatelessWidget {
const MyApp({super.key});

@override
Widget build(BuildContext context) {
return const MaterialApp(
home: HomePage(),
);
}
}

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

@override
State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
late Box<Todo> todosBox;
List<Todo> todosList = [];
bool isLoading = true;

final TextEditingController _titleController = TextEditingController();
final TextEditingController _descriptionController = TextEditingController();

@override
void initState() {
super.initState();
todosBox = Hive.box<Todo>('todos');
loadTodos();
}

/// Loads todos from the Hive box and stores it in todosList.
Future<void> loadTodos() async {
setState(() {
isLoading = true;
});
todosList = todosBox.values.toList();
setState(() {
isLoading = false;
});
}

/// Adds a new todo to the Hive box and updates the todosList.
Future<void> addTodo({
required Todo todo,
}) async {
try {
await todosBox.add(todo);
setState(() {
todosList.add(todo);
});
debugPrint('${todo.title} added successfully');
} catch (e) {
debugPrint('Error: $e');
}
}

/// Deletes a todo from the Hive box and updates the todosList.
Future<void> deleteTodo(int index) async {
try {
await todosBox.deleteAt(index);
setState(() {
todosList.removeAt(index);
});

debugPrint('Todo Removed Successfully');
} catch (e) {
debugPrint('Error: $e');
}
}

/// Changes the completion status of a todo and updates the todosList.
Future<void> changeCompletionStatus(int index, {required bool value}) async {
try {
Todo todo = todosBox.getAt(index)!;
todo.isDone = value;
await todosBox.putAt(index, todo);
setState(() {
todosList[index] = todo;
});
debugPrint('${todo.title} marked ${value ? 'complete' : 'incomplete'}');
} catch (e) {
debugPrint('Error: $e');
}
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Todo List'),
),
body: isLoading
? const Center(
child: CircularProgressIndicator(),
)
: todosList.isEmpty
? const Center(
child: Text("No Todos Created"),
)
: ListView.builder(
shrinkWrap: true,
itemCount: todosList.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(
todosList[index].title,
style: TextStyle(
decoration: todosList[index].isDone
? TextDecoration.lineThrough
: null,
),
),
subtitle: Text(
todosList[index].description,
style: TextStyle(
decoration: todosList[index].isDone
? TextDecoration.lineThrough
: null,
),
),
leading: Checkbox(
value: todosList[index].isDone,
onChanged: (value) {
changeCompletionStatus(index,
value: !todosList[index].isDone);
},
),
trailing: IconButton(
onPressed: () async {
await deleteTodo(index);
},
icon: const Icon(
Icons.delete_forever,
color: Colors.redAccent,
),
),
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () async {
showDialog(
context: context,
builder: (context) {
return SimpleDialog(
title: const Text("Create a Todo"),
contentPadding:
const EdgeInsets.symmetric(vertical: 18, horizontal: 16),
children: [
Column(
children: [
TextField(
controller: _titleController,
decoration: const InputDecoration(
border: OutlineInputBorder(), label: Text("Title")),
),
const SizedBox(
height: 12,
),
TextField(
controller: _descriptionController,
decoration: const InputDecoration(
border: OutlineInputBorder(),
label: Text("Description")),
)
],
),
const SizedBox(height: 22),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
ElevatedButton(
onPressed: () {
Navigator.of(context).pop(widget);
},
child: const Text("CANCEL")),
ElevatedButton(
onPressed: () async {
if (_titleController.text.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
duration: Duration(milliseconds: 200),
content: Text("Title cannot be empty")));
} else {
addTodo(
todo: Todo(
title: _titleController.text,
description: _descriptionController.text,
),
).then((_) {
_titleController.clear();
_descriptionController.clear();
});
}
Navigator.of(context).pop();
},
child: const Text("ADD"),
),
],
)
],
);
},
);
},
child: const Icon(Icons.add),
),
);
}
}

This is how the output will look,

Output of the todo app

Conclusion

In this article, we explored Hive, a lightweight and fast NoSQL database for Flutter applications. We learned how to set up Hive, understood its basic concepts, and generated TypeAdapters for our data models. We also created a simple Todo List app to see how Hive works in action.

Hive is a great choice as it offers a simple and efficient way to manage local data. Even though Hive is no longer maintained it is still used by many developers. Its ease of use, performance, and flexibility make it a great choice.

Happy coding!

https://giphy.com/gifs/fomoduck-duck-fomo-forever-squad-HzPtbOKyBoBFsK4hyc

Building Cenlo | Helping professionals automate time tracking with absolute privacy

See more from me