Beyond Code Generation: Crafting Custom Hive Adapters
Hive is the oldest and most used local database in Flutter. While it’s not recommended to use it since it’s not maintained anymore, if you are a consultant or a freelancer you will get a project which still uses it.
Since Github Copilot has come out, I’ve been migrating off using build_runner
on larger projects because
code generation for 1 change can take a couple of minutes at a time. Projects using hive_generator
fall into that category.
To speed things up, I usually write adapters by hand. You might ask yourself, “why”? Using hive_generator
is good enough.
The pros outweigh the cons by a large margin:
improved
build_runner
speedcleaner models (no annotations)
no need for models only used for database storage
easier migration
manually written adapters are easier to maintain
adapters are easily testable
less code overall
I will show you how you can write adapters manually. Using Github Copilot will speed up this process tenfold.
Ready? Let’s dive in.
Hive Crash Course
Before we delve into writing custom adapters, let's quickly recap the basics of Hive for those new to it.
Hive is a lightweight and fast key-value database for Flutter and Dart, allowing you to persistently store objects locally. Here's a brief rundown of how to use Hive:
Setup: First, you need to add the
hive
andhive_flutter
dependencies to yourpubspec.yaml
file and initialize Hive in your Flutter app.dependencies: hive: ^2.2.3 hive_flutter: ^1.1.0 ## you will not be needing this since you will learn how to write adapters by yourself dev_dependencies: hive_generator: ^2.0.1
Model Definition: Define your data model classes that you want to persist in Hive. When you are using a generator, these classes should extend
HiveObject
and have fields that are supported by Hive.import 'package:hive_flutter/hive_flutter.dart'; part 'user.g.dart'; @HiveType(typeId: 0) class User extends HiveObject { @HiveField(0) final int id; @HiveField(1) final String name; @HiveField(2) final String profilePictureUrl; @HiveField(3) final bool isPremium; User({required this.id, required this.name, required this.profilePictureUrl, required this.isPremium}); }
Adapter Registration: Register adapters for your model classes.
// Initialize Hive await Hive.initFlutter(); // Register adapter for User class Hive.registerAdapter<User>(UserAdapter());
Box: Open a Hive box to store your data. Boxes act like tables in traditional databases and are where you store your objects.
// Open a box named 'users' var box = await Hive.openBox<User>('users'); // If you have a very large collection, use lazyBox await Hive.openLazyBox('users'); final lazyBox = Hive.lazyBox('users');
CRUD Operations: Perform CRUD (Create, Read, Update, Delete) operations on your data by putting, getting, updating, and deleting objects in/from the box.
// Store/update the user in the box await box.put('john', user); // Retrieve the user from the box var retrievedUser = box.get('john'); print(retrievedUser!.name); // Output: John // Delete the user await box.delete('john');
Box close: Close the Box when you are done with using the data.
await box.close();
Writing custom adapters for any class
Custom adapters in Hive allow you to define how objects of your custom classes are serialized and deserialized into binary format for storage. Let's walk through an example of creating a class and writing a custom adapter for it.
I will use the previously mentioned User
class without the Hive annotations:
class User {
final int id;
final String name;
final String profilePictureUrl;
final bool isPremium;
User({
required this.id,
required this.name,
required this.profilePictureUrl,
required this.isPremium,
});
}
To write a custom adapter, you have to 3 things:
extend TypeAdapter with an appropriate type (
User
in this case)give the adapter a
typeId
make sure you are writing and reading properties in the same order
Reading and writing in a different order will cause your data to be corrupt if it’s the same type, or it throws an exception because casting to the type you want will not be possible.
Let’s look at the example of how to properly write an adapter:
import 'package:hive_flutter/hive_flutter.dart';
class UserAdapter extends TypeAdapter<User> {
UserAdapter(this.typeId);
@override
final int typeId;
@override
User read(BinaryReader reader) {
final id = reader.read() as int;
final name = reader.read() as String;
final profilePictureUrl = reader.read() as String;
final isPremium = reader.read() as bool;
return User(
id: id,
name:name,
profilePictureUrl: profilePictureUrl,
isPremium: Premium,
);
}
@override
void write(BinaryWriter writer, User obj) {
writer
..write(obj.id)
..write(obj.name)
..write(obj.profilePictureUrl)
..write(obj.isPremium);
}
}
In the above adapter:
We specify the
typeId
to uniquely identify the adapter.The
read
method reads data from the binary and constructs aUser
object.The
write
method writes theUser
object to the binary.
Now, let's compare the manually written adapter with the code generated by Hive for the User
class.
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'user.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class UserAdapter extends TypeAdapter<User> {
@override
final int typeId = 0;
@override
User read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (var i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return User(
id: fields[0] as int,
name: fields[1] as String,
profilePictureUrl: fields[2] as String,
isPremium: fields[3] as bool,
);
}
@override
void write(BinaryWriter writer, User obj) {
writer
..writeByte(4)
..writeByte(0)
..write(obj.id)
..writeByte(1)
..write(obj.name)
..writeByte(2)
..write(obj.profilePictureUrl)
..writeByte(3)
..write(obj.isPremium);
}
}
The code that we wrote is almost the same expect for one key difference: Hive uses numbers associated with the field to make sure the fields are serialized and deserialized in the correct order. They are also used in case you need to migrate.
Special case: Enums
Hive doesn’t support enums of the box, so we have to serialize them as well. Luckily, we can do this in a very generic way for non-enhanced enums.
There are 2 ways we can solve this problem:
Taking advantage of
values
, which always returns the list in the order that is written in the code. That way we only need to save the index of the value.import 'package:hive_flutter/hive_flutter.dart'; class EnumClassAdapter<T extends Enum> extends TypeAdapter<T> { EnumClassAdapter(this.typeId, this.values); @override final int typeId; final List<T> values; @override T read(BinaryReader reader) { return values[reader.read() as int]; } @override void write(BinaryWriter writer, T obj) { writer.write(obj.index); } }
Taking advantage of
name
property which returns the value of the enum as a String and then searches thevalues
for it, which eliminates the need to store the index correctly.import 'package:hive_flutter/hive_flutter.dart'; class EnumClassAdapter<T extends Enum> extends TypeAdapter<T> { EnumClassAdapter(this.typeId, this.values); @override final int typeId; final List<T> values; @override T read(BinaryReader reader) { final enumString = reader.read() as String; return values.firstWhere((e) => e.name == enumString); } @override void write(BinaryWriter writer, T obj) { writer.write(obj.name); } }
Then all you have to do is register the adapter and just cast the class as usual when writing other adapters:
enum Category { one, two, three }
Hive.registerAdapter<Category>(EnumClassAdapter<Category>(1, Category.values));
await Hive.initFlutter();
I usually chose option number 2 because it is less error-prone. If you choose option 1, you must not change the order of the enum values because your data will become corrupted.
Conclusion
In this article, we've explored the process of writing custom Hive adapters manually. Although code generation tools like hive_generator
exist, manually written adapters offer several advantages, including improved build speed, cleaner models, easier migration, and better maintainability.
By understanding how to write custom adapters, you have greater control over how your data is serialized and deserialized, allowing you to tailor the storage mechanism to your specific needs.
So next time you're working on a Flutter project with Hive, consider crafting your adapters by hand for a more efficient and flexible solution.
If you have found this useful, make sure to like and follow for more content like this. To know when the new articles are coming out, follow me on Twitter or LinkedIn.
Until next time, happy coding!
Subscribe to my newsletter
Read articles from Dinko Marinac directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Dinko Marinac
Dinko Marinac
Mobile app developer and consultant. CEO @ MOBILAPP Solutions. Passionate about the Dart & Flutter ecosystem.