Hive: Make Hive working with @immutable class

Created on 14 Feb 2020  Â·  29Comments  Â·  Source: hivedb/hive

@leisim Hi there!
It will be awesome to get this awesome plugin Freezed working with immutable models, so we can have immutable model that can be generated and stored directly in Hive database.

cc: @rrousselGit

enhancement

Most helpful comment

Thanks, I'll take a look. It will be hard to achieve this with the changes planned for Hive 2.0 but I'll try to figure it out.

All 29 comments

Thanks, I'll take a look. It will be hard to achieve this with the changes planned for Hive 2.0 but I'll try to figure it out.

That would be great! I remember some time ago trying to tag my models as @immutable but got warnings as HiveObjects are mutable

@frank06 exactly that!

I suppose that instead of "working with Freezed", it's more of a "working with immutable models".

Freezed doesn't do anything fancy.

I'd also really love this to work. I'll try to find some time to look into the issues that occur when using hive with freezed. It would make our data classes so much shorter!

EDIT: I just started looking into it, but because the hive_generator repo got archived, I decided it's not worth the effort.


Hand-written immutable Hive data classes

@HiveType(typeId: TypeId.user)
class User implements Entity<User> {
  const User({
    @required this.id,
    @required this.firstName,
    @required this.lastName,
    @required this.email,
    @required this.schoolId,
    String displayName,
    @required this.avatarInitials,
    @required this.avatarBackgroundColor,
    @required this.permissions,
    @required this.roleIds,
  })  : assert(id != null),
        assert(firstName != null),
        assert(lastName != null),
        assert(email != null),
        assert(schoolId != null),
        displayName = displayName ?? '$firstName $lastName',
        assert(avatarInitials != null),
        assert(avatarBackgroundColor != null),
        assert(permissions != null),
        assert(roleIds != null);

  User.fromJson(Map<String, dynamic> data)
      : this(
          id: Id<User>(data['_id']),
          firstName: data['firstName'],
          lastName: data['lastName'],
          email: data['email'],
          schoolId: data['schoolId'],
          displayName: data['displayName'],
          avatarInitials: data['avatarInitials'],
          avatarBackgroundColor:
              (data['avatarBackgroundColor'] as String).hexToColor,
          permissions: (data['permissions'] as List<dynamic>).cast<String>(),
          roleIds: parseIds(data['roles']),
        );

  static Future<User> fetch(Id<User> id) async =>
      User.fromJson(await services.api.get('users/$id').json);

  @override
  @HiveField(0)
  final Id<User> id;

  @HiveField(1)
  final String firstName;

  @HiveField(2)
  final String lastName;

  String get shortName => '${firstName.chars.first}. $lastName';

  @HiveField(3)
  final String email;

  @HiveField(4)
  final String schoolId;

  @HiveField(5)
  final String displayName;

  @HiveField(7)
  final String avatarInitials;
  @HiveField(8)
  final Color avatarBackgroundColor;

  @HiveField(6)
  final List<String> permissions;
  bool hasPermission(String permission) => permissions.contains(permission);

  @HiveField(9)
  final List<Id<Role>> roleIds;
  bool get isTeacher => hasRole(Role.teacherName);
  bool hasRole(String name) {
    // TODO(marcelgarus): Remove the hard-coded mapping and use runtime lookup when upgrading flutter_cached and flattening is supported.
    final id = {
      Role.teacherName: '0000d186816abba584714c98',
    }[name];
    return id != null && roleIds.contains(Id<Role>(id));
  }

  @override
  bool operator ==(Object other) =>
      other is User &&
      id == other.id &&
      firstName == other.firstName &&
      lastName == other.lastName &&
      email == other.email &&
      schoolId == other.schoolId &&
      displayName == other.displayName &&
      avatarInitials == other.avatarInitials &&
      avatarBackgroundColor == other.avatarBackgroundColor &&
      permissions.deeplyEquals(other.permissions, unordered: true) &&
      roleIds.deeplyEquals(other.roleIds, unordered: true);
  @override
  int get hashCode => hashList([
        id,
        firstName,
        lastName,
        email,
        schoolId,
        displayName,
        avatarInitials,
        avatarBackgroundColor,
        permissions,
        roleIds
      ]);
}


Using freezed with Hive (doesn't work yet)

@freezed
@HiveType(typeId: TypeId.user)
abstract class User implements Entity<User>, _$User {
  const User._();
  const factory User({
    @HiveField(0) @required Id<User> id,
    @HiveField(1) @required String firstName,
    @HiveField(2) @required String lastName,
    @HiveField(3) @required String email,
    @HiveField(4) @required String schoolId,
    @HiveField(5) String displayName,
    @HiveField(7) @required String avatarInitials,
    @HiveField(8) @required Color avatarBackgroundColor,
    @HiveField(6) @required List<String> permissions,
    @HiveField(9) @required List<Id<Role>> roleIds,
  }) = _User;
  // displayName = displayName ?? '$firstName $lastName',

  static User fromJson(Map<String, dynamic> data) => User(
        id: Id<User>(data['_id']),
        firstName: data['firstName'],
        lastName: data['lastName'],
        email: data['email'],
        schoolId: data['schoolId'],
        displayName: data['displayName'],
        avatarInitials: data['avatarInitials'],
        avatarBackgroundColor:
            (data['avatarBackgroundColor'] as String).hexToColor,
        permissions: (data['permissions'] as List<dynamic>).cast<String>(),
        roleIds: parseIds(data['roles']),
      );

  static Future<User> fetch(Id<User> id) async =>
      User.fromJson(await services.api.get('users/$id').json);

  String get shortName => '${firstName.chars.first}. $lastName';

  bool hasPermission(String permission) => permissions.contains(permission);

  bool get isTeacher => hasRole(Role.teacherName);
  bool hasRole(String name) {
    // TODO(marcelgarus): Remove the hard-coded mapping and use runtime lookup when upgrading flutter_cached and flattening is supported.
    final id = {
      Role.teacherName: '0000d186816abba584714c98',
    }[name];
    return id != null; // && roleIds.contains(Id<Role>(id));
  }
}

I'd also really love this to work. I'll try to find some time to look into the issues that occur when using hive with freezed. It would make our data classes so much shorter!

EDIT: I just started looking into it, but because the hive_generator repo got archived, I decided it's not worth the effort.

Hand-written immutable Hive data classes
Using freezed with Hive (doesn't work yet)

Any update about this feature?

Sure. I have a kind of love-hate relationship with the Hive serializer. I think it got a lot of things right, but I also strongly disagree with some of the design decisions (like the format not being self-descriptive, like JSON, or the adapters having the responsibility to define their own ids, causing IDs to be scattered all over your project instead of being defined in one central place). I also wanted to use the serializer on its own separate from Hive, so in #152 I did some brainstorming with @leisim about how the serializer could be improved – and several great ideas came up. I wanted to play around with some of them, so I started writing a serializer with a slightly different approach, which ended up in tape. I also focused on great tooling, but it's not production-ready by a long shot. For the Hive replacement – isar – @leisim wrote a completely new serializer with some different design philosophies which also looks promising.

Tape does work with freezed, and I think the part of being able to use freezed could also be applied to Hive – I'm currently quite busy with studying and work stuff as well as other projects, so it's probably gonna take some time before I find time to look at it though. If anyone has spare time and feels like it, don't hesitate to code something up and file a PR. Tape already works with freezed, so in the repo you should find some guidance on how that could work. I'm also available for concrete questions, if you have any.

Any updates?

What do you mean by "working with @immutable class"? I haven't used freezed package, so I don't know what's exact issue.

"@immutable class" is not about freezed - you could read thread history for more details
freezed just example
it is just Hive blocker issue that force many users to use Sembast instead

"@immutable class" is not about freezed - you could read thread history for more details
freezed just example
it is just Hive blocker issue that force many users to use Sembast instead

I have tried adding @immutable to my hive models. But did not get any warnings or errors.

Does the issue is related to model adapters generated by hive_generator or caused when extending models with HiveObject?

image

@immutable to my hive models
"@immutable classes" usually constructed using with or implements
so it will compile and run but it will store empty objects in the Hive DB.

I like Hive so hope it will be fixed once.

for example with freezed immutable class looks like:

import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hive/hive.dart';

part 'immutable_class.freezed.dart';
part 'immutable_class.g.dart';

@freezed
abstract class ImmutableClass with _$ImmutableClass {
  @HiveType(typeId: 5)
  const factory ImmutableClass({
    @JsonKey(name: 'id', required: true, disallowNullValue: true) @HiveField(0) int id,
    @HiveField(1) int someField1,
    @HiveField(2) String someField2,
  }) = _ImmutableClass;

  factory ImmutableClass.fromJson(Map<String, dynamic> json) => _$ImmutableClassFromJson(json);
}

I'm not planning to implement this feature. The implementation will be package specific (freezed in your example). Instead you can write your own hive adapter. Or at least human readable model classes (which hive generator currently supports). Fyi: hive_generator generates code for fields annotated with HiveField inside classes mentioned with HiveType. It doesn't checks for constructor arguments or something else.

Dart have no @immutable classes other then packages specific built_value freezed and etc (@immutable attribute only will not class immutable)
So user have to choose reliable code with specific packages or non-reliable code with Hive - therefore many forced to use Sembast instead of Hive

moreover

hive_generator generates code for fields annotated with HiveField inside classes mentioned with HiveType.

-- freezed generate fields annotated with HiveField inside classes mentioned with HiveType (as base class or mixin)
-- and Hive still do not save objects correctly

the only way I found to workarround it some-how, but it looks quite ugly with adapter name string, hope Hive will be improved to fix it:

import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hive/hive.dart';

part 'immutable_class.freezed.dart';
part 'immutable_class.g.dart';

@freezed
abstract class ImmutableClass with _$ImmutableClass {
  @HiveType(typeId: 5, adapterName: 'ImmutableClassAdapter')
  const factory ImmutableClass({
    @JsonKey(name: 'id', required: true, disallowNullValue: true) @HiveField(0) int id,
    @HiveField(1) int someField1,
    @HiveField(2) String someField2,
  }) = _ImmutableClass;

  factory ImmutableClass.fromJson(Map<String, dynamic> json) => _$ImmutableClassFromJson(json);
}

What do you think about that, @leisim ?

the only way I found to workarround it some-how, but it looks quite ugly with adapter name string, hope Hive will be improved to fix it:

import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hive/hive.dart';

part 'immutable_class.freezed.dart';
part 'immutable_class.g.dart';

@freezed
abstract class ImmutableClass with _$ImmutableClass {
  @HiveType(typeId: 5, adapterName: 'ImmutableClassAdapter')
  const factory ImmutableClass({
    @JsonKey(name: 'id', required: true, disallowNullValue: true) @HiveField(0) int id,
    @HiveField(1) int someField1,
    @HiveField(2) String someField2,
  }) = _ImmutableClass;

  factory ImmutableClass.fromJson(Map<String, dynamic> json) => _$ImmutableClassFromJson(json);
}

@TheMisir I have been using this solution since last 3-4 months. And I didn't get any issues.
The problem with using Hive with Freezed is, Freezed creates Private classes (_$*), so for example, our class Product will have adapter called _$ProductAdapter, which we won't be able to access anywhere.

The solution is to manually provide adapterName just as this solution and it would work PERFECTLY.

So, to conclude, neither of the libraries are at fault.

It's ugly to use String as name
What about trim start '_' and '$' symbols from adapter name by default? As adapter should not be private

So the problem is adapter names. I can trim _$ part from adapter names.

I can trim _$ part from adapter names.

It could be awesome, thanks a lot

Published to pub.dev. You will need to set min version to 0.7.2+1:

dev_dependencies:
  hive_generator: ^0.7.2+1

@TheMisir

Adapter still have '_' sign:

class _SomeClassAdapter extends TypeAdapter<_$_SomeClass> {

Thanks for the fast update, hope you will fix it as adapter name started with '_' sign could not be really used

Thanks in advance

@TheMisir

Adapter still have '_' sign:

class _SomeClassAdapter extends TypeAdapter<_$_SomeClass> {

Thanks for the fast update, hope you will fix it as adapter name started with '_' sign could not be really used

Thanks in advance

Oops 😅 Now fixing.

Fixed and published!

Wow, Adapters are generated correctly, awesome, thanks a lot for the fast fix!

any update to the docs, regarding these changes?

is it possible to have sth like this for union classes ?

HIVE

@HiveType(typeId: 2)
enum AudioOrderDao {
  @HiveField(0)
  order,

  @HiveField(2)
  repeatAll,

  @HiveField(1)
  repeatOne,

  @HiveField(3)
  shuffle
}

FREEZED

@freezed
abstract class AudioOrder with _$AudioOrder {
  const factory AudioOrder.order() = _Order;
  const factory AudioOrder.repeatAll() = _RepeatAll;
  const factory AudioOrder.repeatOne() = _RepeatOne;
  const factory AudioOrder.shuffle() = _Shuffle;
}

thanks

is it possible to have sth like this for union classes ?

HIVE

@HiveType(typeId: 2)
enum AudioOrderDao {
  @HiveField(0)
  order,

  @HiveField(2)
  repeatAll,

  @HiveField(1)
  repeatOne,

  @HiveField(3)
  shuffle
}

FREEZED

@freezed
abstract class AudioOrder with _$AudioOrder {
  const factory AudioOrder.order() = _Order;
  const factory AudioOrder.repeatAll() = _RepeatAll;
  const factory AudioOrder.repeatOne() = _RepeatOne;
  const factory AudioOrder.shuffle() = _Shuffle;
}

thanks

The first is enum and second one is class. They are not same thing. Anyways you can create custom type adapter for that.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

rupamking1 picture rupamking1  Â·  3Comments

Ferdzzzzzzzz picture Ferdzzzzzzzz  Â·  3Comments

yannickvg picture yannickvg  Â·  4Comments

azilvl picture azilvl  Â·  3Comments

yaymalaga picture yaymalaga  Â·  4Comments