isar / hive

Lightweight and blazing fast key-value database written in pure Dart.
Apache License 2.0
4.02k stars 401 forks source link

When get List<T> Hive V4 throws 'Type Mismatch' error #1302

Open serifmehmet opened 1 month ago

serifmehmet commented 1 month ago

I have tried to make module from Hive with Generic models. Created 'CacheModel' mixin like this:

mixin CacheModel {
  String get id;

  CacheModel fromDynamicJson(dynamic json);
  Map<String, dynamic> toJson();
}

And a manager to registerAdapters like this:

final class HiveCacheManager extends CacheManager {
  HiveCacheManager({super.path});

  @override
  Future<void> init({required List<CacheModel> cacheModels}) async {
    final documentPath =
        path ?? (await getApplicationDocumentsDirectory()).path;
    Hive.defaultDirectory = documentPath;

    for (final cacheModel in cacheModels) {
      Hive.registerAdapter(
          '${cacheModel.runtimeType}', cacheModel.fromDynamicJson);
    }
  }

  @override
  void remove() {
    Hive.deleteAllBoxesFromDisk();
  }
}

CacheManager is an abstract class and have a CacheOperation abstract class like this:

abstract class CacheOperation<T extends CacheModel> {
  void add(T item);
  void addAll(List<T> items);
  void remove(String id);

  void clear();
  List<T> getAll();
  T? get(String id);
  T? getFirst();
}

I also have a HiveCacheOperation which extends CacheOperation of T Type like this:

final class HiveCacheOperation<T extends CacheModel> extends CacheOperation<T> {
  HiveCacheOperation() {
    _box = Hive.box<T>(name: T.toString());
  }

  late final Box<T> _box;
  @override
  void add(T item) {
    _box.put(item.id, item);
  }

  @override
  void addAll(List<T> items) {

    _box.putAll(Map.fromIterable(
      items,
      key: (element) => (element as T).id,
      value: (element) => (element as T),
    ));

  }

  @override
  void clear() {
    _box.clear();
  }

  @override
  T? get(String id) {
    return _box.get(id);
  }

  @override
  T? getFirst() {
    return _box.get(_box.keys.first);
  }

  bool countBoxKeys() {
    if (_box.isOpen) {
      final count = _box.getAll(_box.keys).cast<T>().isNotEmpty;

      return count;
    }

    return false;
  }

  @override
  List<T> getAll() {
    return _box
        .getAll(_box.keys)
        .where((element) => element != null)
        .cast<T>()
        .toList();
  }

  @override
  void remove(String id) {
    _box.delete(id);
  }
}

I created a UserCacheModel and extend it from CacheModel like this:

final class UserCacheModel with CacheModel {
  UserCacheModel({
    required this.user,
  });

  UserCacheModel.empty() : user = const UserModel();

  final UserModel user;

  @override
  CacheModel fromDynamicJson(json) {
    final mapCast = json as Map<String, dynamic>?;
    if (mapCast == null) return this;

    return copyWith(userModel: UserModel.fromJson(mapCast));
  }

  @override
  String get id => user.id.toString();

  @override
  Map<String, dynamic> toJson() {
    return user.toJson();
  }

  UserCacheModel copyWith({UserModel? userModel}) {
    return UserCacheModel(user: userModel ?? user);
  }
}

Finally created a ProductCache which required CacheManager like this:

final class ProductCache {
  ProductCache({required CacheManager cacheManager})
      : _cacheManager = cacheManager;

  final CacheManager _cacheManager;

  Future<void> init() async {
    await _cacheManager.init(
      cacheModels: [
        CorporationCacheModel(corporation: const CorporationModel()),
        UserCacheModel(user: const UserModel()),
      ],
    );
  }

  late final HiveCacheOperation<UserCacheModel> userCacheOperation =
      HiveCacheOperation<UserCacheModel>();

  late final HiveCacheOperation<CorporationCacheModel>
      corporationCacheOperation = HiveCacheOperation<CorporationCacheModel>();

}

But when i try to call .getAll function of userCacheOperation i got "Invalid argument(s): Type mismatch. Expected UserCacheModel but got CorporationCacheModel.".

So is this a registerAdapter error or something else?

ahmtydn commented 6 days ago

Resolving Type Mismatch Error in Hive with Generic Models

The error "Invalid argument(s): Type mismatch. Expected UserCacheModel but got CorporationCacheModel." typically occurs when there's a mix-up with how adapters are registered or how Hive boxes are opened with specific types. This issue is often related to improper handling of type registrations or incorrect usage of Hive's box and adapter mechanisms, especially when dealing with generic models and mixins.

Root Cause of the Issue

The problem arises because the adapters for different types (UserCacheModel, CorporationCacheModel, etc.) might not be correctly registered, leading Hive to confuse one type for another. Specifically, Hive needs each type to be uniquely identified and registered with its corresponding adapter function.

Solution Implemented in the PR

In the pull request #1310 , the issue has been resolved by ensuring that each CacheModel type is registered with Hive uniquely and appropriately.

Key Changes in the PR:

  1. Added Type Parameter in Adapter Registration:

    • A Type parameter was added to ensure that each adapter is registered uniquely based on its runtime type. This prevents type mismatches by explicitly linking the adapter with the correct model type.
  2. Correct Use of registerAdapter Method:

    • The registerAdapter method was modified to include type safety checks and ensure that each type's adapter is only registered once.
  3. Enhanced Adapter Registration Logic:

    • The registration process was updated to use a specific identifier (like the runtime type) to differentiate between various models when registering adapters.

How to Use the Solution

To use the fixed version of Hive with your generic models, follow these steps:

  1. Update Your Dependency:

    • Use the specific branch or commit from the PR to pull the updated version of Hive that contains the fix:
    
    dependencies:
     flutter:
       sdk: flutter
     hive:
       git:
         url: https://github.com/your-username/hive.git
         ref: 5eb6fe59fdb70d332a27102ecccd4efc0bfde7fd
  2. Initialize the Cache Manager Correctly:

Make sure your HiveCacheManager is set up as follows to initialize and register the adapters properly:

final class HiveCacheManager extends CacheManager {
  HiveCacheManager({super.path});

  @override
  Future<void> init({required List<CacheModel> cacheModels}) async {
    final documentPath = path ?? (await getApplicationDocumentsDirectory()).path;
    Hive.defaultDirectory = documentPath;

    for (final cacheModel in cacheModels) {
      Hive.registerAdapter(
        '${cacheModel.runtimeType}', 
        cacheModel.fromDynamicJson, 
        cacheModel.runtimeType,  // Ensure the correct type is registered
      );
    }
  }

  @override
  void remove() {
    Hive.deleteAllBoxesFromDisk();
  }
}
  1. Verify Usage in Your ProductCache:

Ensure that each operation on your cache models, like getAll, correctly references the unique adapters registered for those models:

final class ProductCache {
  ProductCache({required CacheManager cacheManager})
      : _cacheManager = cacheManager;

  final CacheManager _cacheManager;

  Future<void> init() async {
    await _cacheManager.init(
      cacheModels: [
        CorporationCacheModel(corporation: const CorporationModel()),
        UserCacheModel(user: const UserModel()),
      ],
    );
  }

  late final HiveCacheOperation<UserCacheModel> userCacheOperation =
      HiveCacheOperation<UserCacheModel>();

  late final HiveCacheOperation<CorporationCacheModel> corporationCacheOperation =
      HiveCacheOperation<CorporationCacheModel>();
}

[!NOTE]
By following these steps and using the changes implemented in the PR, you should be able to resolve the type mismatch error and use the getAll function correctly with your generic models in Hive. If you encounter further issues, ensure that each model's adapter is correctly registered and that no adapters are overridden during initialization.

[!WARNING]
For the solution to work correctly, ensure to clean the Hive cache once initially using Hive.deleteAllBoxesFromDisk() before registering the adapters. This step helps to avoid type conflicts from previously cached data.

[!TIP]
And hey, if you'd like to buy me a coffee, your code might just run even better! 😉