Beyond Code Generation: Crafting Custom Hive Adapters
Tags: flutter, dart, nosql, database
Published: 2024-04-23 08:35:45

[Hive](https://pub.dev/packages/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`](https://pub.dev/packages/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` speed
    
* cleaner 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:

1. **Setup**: First, you need to add the `hive` and `hive_flutter` dependencies to your `pubspec.yaml` file and initialize Hive in your Flutter app.
    
    ```yaml
    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
    ```
    
2. **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.
    
    ```dart
    	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});
    	}
    ```
    
3. **Adapter Registration**: Register adapters for your model classes.
    
    ```dart
    // Initialize Hive
    await Hive.initFlutter();
    
    // Register adapter for User class
    Hive.registerAdapter<User>(UserAdapter());
    ```
    
4. **Box**: Open a Hive box to store your data. Boxes act like tables in traditional databases and are where you store your objects.
    
    ```dart
    // 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');
    ```
    
5. **CRUD Operations**: Perform CRUD (Create, Read, Update, Delete) operations on your data by putting, getting, updating, and deleting objects in/from the box.
    
    ```dart
    // 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');
    ```
    
6. Box close: Close the Box when you are done with using the data.
    
    ```dart
    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:

```dart
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:

```dart
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 a `User` object.
    
* The `write` method writes the `User` object to the binary.
    

Now, let's compare the manually written adapter with the code generated by Hive for the `User` class.

```dart
// 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:

1. 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.
    
    ```dart
    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);
      }
    }
    ```
    
2. Taking advantage of `name` property which returns the value of the enum as a String and then searches the `values` for it, which eliminates the need to store the index correctly.
    
    ```dart
    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:

```dart
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**](https://twitter.com/dinkomarinac) or [**LinkedIn**](https://www.linkedin.com/in/dinko-marinac/).

Until next time, happy coding!

Reposted from my [blog](https://dinkomarinac.dev/beyond-code-generation-crafting-custom-hive-adapters).