isar / hive

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

Map<String,dynamic> from Hive doesn't behave like a proper Map<String,dynamic> #522

Open moseskarunia opened 3 years ago

moseskarunia commented 3 years ago

Version

Hello guys, so I'm facing a difficulty understanding what type of Map does Hive returns. Below is my code:

Code sample

Future<List<Map<String, dynamic>>> readFromHive() async {
  final box = await Hive.openBox('storageName');
  final result = box.toMap().map(
        (k, e) => MapEntry(
          k.toString(),
          Map<String, dynamic>.from(e),
        ),
      );

  return result.values.toList();

  /// Let's say the result is:
  /// ```
  /// [ { name: 'John Doe', email: 'johndoe@gmail.com'} ]
  /// ```
}

final results = await readFromHive();

print(results[0]); // { name: John Doe, email: johndoe@gmail.com}
print(results[0]['name']); // null
print(results[0]['email']); // null
print(results[0].runtimeType) // _InternalLinkedHashMap<String, dynamic>

Here's the weird things:

  1. If it's not a Map<String,dynamic> why doesn't it crash the moment it exits the readFromHive()?
  2. If it's a Map<String,dynamic> why does print(results[0]['name']); prints null, while print(results[0]) prints correct result?
  3. I actually uses @JsonSerializable package. And I always need to pass anyMap: true in order for the fromJson to work properly. I never feels to pass anyMap: true if I don't deal with Hive.
  4. I use VS Code, and I put a breakpoint on the print statement. When I inspect the type of results[0], the type truly says Map<String,dynamic>

So, the question is like the title, why does it feels like Hive Map<String,dynamic> is not really acts like a Map<String,dynamic>? And how to make it a "true" `Map<String,dynamic>? Or is this intended? Maybe I misunderstand how Hive works?

Thanks

P.S. I can't use Hive's built in json parser since in a clean architecture, the entity and data source should not even care which local storage library I use.

themisir commented 3 years ago

Why you're not using Hive.box(..).values?

Future<List<Map<String, dynamic>>> readFromHive() async {
  final box = await Hive.openBox('storageName');
  final result = box.values.cast<Map<String, dynamic>>();
  return result;

  /// Let's say the result is:
  /// ```
  /// [ { name: 'John Doe', email: 'johndoe@gmail.com'} ]
  /// ```
}
moseskarunia commented 3 years ago
box.values.cast<Map<String, dynamic>>()

Hi, thanks for the response @TheMisir , it worked. Thanks.

moseskarunia commented 3 years ago

@TheMisir turns out crashes on the real usage (succeed on test)

try {
  final box = await hive.openBox(storageName);
  return box.values.cast<Map<String, dynamic>>().toList(); // crashes here
catch (e) {
  throw CleanException(name: 'UNEXPECTED_ERROR', group: 'hive'); // Got here
}

Error

_CastError (type '_InternalLinkedHashMap<dynamic, dynamic>' is not a subtype of type 'Map<String, dynamic>' in type cast)


However, this works

final box = await hive.openBox(storageName);
final result = box.toMap().map(
  (k, e) => MapEntry(
    k.toString(),
    Map<String, dynamic>.from(e),
  ),
);

return result.values.toList();
Harrys76 commented 3 years ago

@TheMisir turns out crashes on the real usage (succeed on test)

try {
  final box = await hive.openBox(storageName);
  return box.values.cast<Map<String, dynamic>>().toList(); // crashes here
catch (e) {
  throw CleanException(name: 'UNEXPECTED_ERROR', group: 'hive'); // Got here
}

Error

_CastError (type '_InternalLinkedHashMap<dynamic, dynamic>' is not a subtype of type 'Map<String, dynamic>' in type cast)

However, this works

final box = await hive.openBox(storageName);
final result = box.toMap().map(
  (k, e) => MapEntry(
    k.toString(),
    Map<String, dynamic>.from(e),
  ),
);

return result.values.toList();

still got error type '_InternalLinkedHashMap<dynamic, dynamic>' is not a subtype of type 'Map<String, dynamic>' in type cast

my code

Stream<List<Map<String,` dynamic>>> watchList(T dataSource) async* {
    final _box = await _openBox(dataSource);
    final result = _box.toMap().map((key, value) =>
        MapEntry(key.toString(), Map<String, dynamic>.from(value)));
    yield* _box.watch().map((event) => result.values.toList());
  }
moseskarunia commented 3 years ago

@Harrys76

I'm currently using Hive with a package named freezed and it seems to work seamlessly.

themisir commented 3 years ago

@Harrys76 due to dart's type system limitations, Hive can not persist generic types. Instead of Map<String, dynamic> hive returns Map<dynamic, dynamic> so you have to cast it manually.

Harrys76 commented 3 years ago

@moseskarunia @TheMisir could you help me to solve it?

I sometimes got 'subtype of type' error when run this code

Stream<List<Map<String,` dynamic>>> watchList(T dataSource) async* {
    final _box = await _openBox(dataSource);
    yield* _box.watch().map((event) => _box.values.toList());
  }

the error occur in _box.values.toList()

moseskarunia commented 3 years ago

@moseskarunia @TheMisir could you help me to solve it?

I sometimes got 'subtype of type' error when run this code

Stream<List<Map<String,` dynamic>>> watchList(T dataSource) async* {
    final _box = await _openBox(dataSource);
    yield* _box.watch().map((event) => _box.values.toList());
  }

the error occur in _box.values.toList()

Parse the map object with freezed first to produce a new sane list of dart object. Don't access it directly.

Harrys76 commented 3 years ago

@moseskarunia

i tried two things:

  1. make a freezed class (BoxRecords) and parse the value from _box.values like this:

    Stream<List<BoxRecords>> watchList(T dataSource) async* {
    final _box = await _openBox(dataSource);
    yield* _box.watch().map((event) => _box.values.map((value) => BoxRecords(records: value)).toList());
    }
    1. use BoxRecords as TypeAdapter
      Stream<List<BoxRecords>> watchList(T dataSource) async* {
      final _box = await _openBox(dataSource);
      yield* _box.watch().map((event) => _box.values.toList());
      }

    still got this error:

    /flutter (27820): type '_InternalLinkedHashMap<dynamic, dynamic>' is not a subtype of type 'BoxRecords' in type cast
    I/flutter (27820): 
    I/flutter (27820): #0      Keystore.getValues.<anonymous closure> (package:hive/src/box/keystore.dart:122:45)
    I/flutter (27820): #1      MappedIterator.moveNext (dart:_internal/iterable.dart:392:20)
    I/flutter (27820): #2      new List.from (dart:core-patch/array_patch.dart:50:19)
    I/flutter (27820): #3      new List.of (dart:core-patch/array_patch.dart:68:17)
    I/flutter (27820): #4      Iterable.toList (dart:core/iterable.dart:404:12)
    I/flutter (27820): #5      DataSourceRecordBox.watchList.<anonymous closure> (package:jojotask/src/repositories/cache/hive/box/form_record_box.dart:35:52)
    I/flutter (27820): #6      _MapStream._handleData (dart:async/stream_pipe.dart:219:31)
    I/flutter (27820): #7      _ForwardingStreamSubscription._handleData (dart:async/stream_pipe.dart:157:13)
    I/flutter (27820): #8      _rootRunUnary (dart:async/zone.dart:1198:47)
    I/flutter (27820): #9      _CustomZone.runUnary (dart:async/zone.dart:1100:19)
    I/flutter (27820): #10     _CustomZone.runUnaryGuarded (dart:async/zone.dart:1005:7)
    I/flutter (27820): #11     _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:357:11)
    I/flutter (27820): #12     _DelayedData.perform (dart:async/stream_impl.dart:611:14)
    I/flutter (27820): #13     _StreamImplEvents.handleNext (dart:async/stream_impl
moseskarunia commented 3 years ago

@Harrys76 you have to parse it using the freezed class's fromJson function (annotate your factory using @JsonSerializable, with at least both of anyMap and checked set to true) and then add factory from json to your freezed class, calling generated fromJson code. That's what makes it work in my case

Harrys76 commented 3 years ago

thank you @moseskarunia for pointing out a solution. Parsing with fromJson() from freezed class is works. I already try to reproduce the error by kill app and open it again, the error gone.

so in my case, I'm using BoxRecords class as TypeAdapter and this is my BoxRecordsAdapter class:

@freezed
abstract class BoxRecords with _$BoxRecords {
  static const fromJsonFactory = _$BoxRecordsFromJson;

  @JsonSerializable(anyMap: true, checked: true)
  factory BoxRecords({
    Map<String, dynamic> record,
  }) = _BoxRecords;

  factory BoxRecords.fromJson(Map<String, dynamic> json) =>
      _$BoxRecordsFromJson(json);
}
class BoxRecordsAdapter extends TypeAdapter<BoxRecords> {
  @override
  final typeId = 0;

  @override
  BoxRecords read(BinaryReader reader) {
    final result = Map<String, dynamic>.from(reader.read());
    return BoxRecords.fromJson({'records': result});
  }

  @override
  void write(BinaryWriter writer, BoxRecords obj) {
    writer.write<Map<String, dynamic>>(obj.records);
  }
}
moseskarunia commented 3 years ago

@TheMisir with all due respect, solving it using other library doesn't solve the actual problem within the library itself. Therefore, I'm reopening it.

If I need to cast it myself with just the built-in cast, then sure it's solved. But it isn't possible without copying freezed / json_serializable parsing logic (which is cumbersome to do without code generation).

rokk4 commented 3 years ago

I had similar problems, we have a LOT of DTO Classes, many of them are deeply nested and also Unions generated with freezed.
What works for me is that I use the .to/fromJson() of the classes wrap them with a Class that has a generated TypeAdapter. Directly using the Map<String,dynamic> of .to/fromJson() with a Box<Map<String, dynamic>> or Box<Map<String, dynamic>?> produced the "not a subtype errors", AFTER restart. Also the suggested cast() etc. did not work for me.

This works for me:

 Box<DtoJsonHiveWrapper> _box = await Hive.openBox(
      "box",
      encryptionCipher: HiveAesCipher(encryptionKey),
    );
import 'package:hive_flutter/hive_flutter.dart';

part 'dto_hive_wrapper.g.dart';

@HiveType(typeId: 7)
class DtoJsonHiveWrapper {
  @HiveField(0)
  final Map<String, dynamic> dtoJson;

  DtoJsonHiveWrapper({required this.dtoJson});
}
[✓] Flutter (Channel beta, 2.5.0-5.2.pre, on macOS 11.5.1 20G80 darwin-arm, locale en-DE)
    • Flutter version 2.5.0-5.2.pre at /Users/r0/flutter
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision 19c61fed0d (12 days ago), 2021-08-18 17:10:31 -0700
    • Engine revision 7a4c4505f6
    • Dart version 2.14.0 (build 2.14.0-377.7.beta)
logic-and-math commented 3 years ago

@rokk4 This solution doesn't work for nested classes in my case.

Nico04 commented 2 years ago

I have just setup Hive, I'm having the same issue here. I'm storing Map<String, dynamic> in the box, but I've got a cast exception when reading values. Any idea how to solve this, after more than a year ?

EDIT : I have the same issue with toMap() alone

type '_InternalLinkedHashMap<dynamic, dynamic>' is not a subtype of type 'Map<String, dynamic>' in type cast
BoxImpl.toMap (package:hive/src/box/box_impl.dart:102:36)
pckimlong commented 2 years ago

For me I can't found any solution rather than doing quick fix jsonEncode then decode it back. This worked for me even I have nested map inside map, But performance is my concern.

Map<String, dynamic> _parser(dynamic hiveMap){
  final jsonString = jsonEncode(hiveMap);
  return jsonDecode(jsonString);
}
Future<AppSetting?> getAppSetting() async {
    try {
      final result = await _box.get(_appSetting);
      if (result == null) return null;
      return AppSetting.fromJson(_parser(result));
    } on Exception catch (e) {
      debugPrint(e.toString());
      return null;
    }
  }
pedropacheco92 commented 2 years ago

I've added a custom type adapter that worked for me, in nested Map structures. I generated a adapter and just extended it and overrode the read method for this:


@override
  FooBar read(BinaryReader reader) {
    final numOfFields = reader.readByte();
    final fields = {
      for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
    };

    return FooBar(
      fields[0] as String,
      _castInternalMap(fields[1]), // changed just this
    );
  }

  dynamic _castInternalMap(dynamic value) {
    if (value != null) {
      if (value is Map) {
        return value.cast().map(
              (key, value) => MapEntry(key, _castInternalMap(value)),
        );
      }
      if (value is List) {
        return value.map(_castInternalMap).toList();
      }
    }
    return value;
  }

The _castInternalMap will check the value and handle it recursively