Skip to content
6 changes: 2 additions & 4 deletions packages/hydrated_bloc/lib/src/hydrated_bloc.dart
Original file line number Diff line number Diff line change
Expand Up @@ -324,10 +324,8 @@ mixin HydratedMixin<State> on BlocBase<State> {
_checkCycle(object);
final map = <String, dynamic>{};
object.forEach((dynamic key, dynamic value) {
final castKey = _cast<String>(key);
if (castKey != null) {
map[castKey] = _traverseWrite(value).value;
}
final castKey = key?.toString();
if (castKey != null) map[castKey] = _traverseWrite(value).value;
});
_removeSeen(object);
return map;
Expand Down
1 change: 1 addition & 0 deletions packages/hydrated_bloc/test/cubits/cubits.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ export 'from_json_state_cubit.dart';
export 'json_serializable_cubit.dart';
export 'list_cubit.dart';
export 'manual_cubit.dart';
export 'season_palette_cubit.dart';
export 'simple_cubit.dart';
91 changes: 91 additions & 0 deletions packages/hydrated_bloc/test/cubits/season_palette_cubit.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:meta/meta.dart';

/// A cubit that has a state which uses int key values in its serialized form.
/// https://github.com/felangel/bloc/issues/3983
class SeasonPaletteCubit extends HydratedCubit<SeasonPalette> {
SeasonPaletteCubit() : super(const SeasonPalette({}));

void update(SeasonPalette palette) => emit(palette);

@override
Map<String, dynamic> toJson(SeasonPalette state) => state.toJson();

@override
SeasonPalette fromJson(Map<String, dynamic> json) {
return SeasonPalette.fromJson(json);
}
}

@immutable
class SeasonPalette {
const SeasonPalette(this.colors);

factory SeasonPalette.fromJson(Map<String, dynamic> json) {
final deserialized = json['colors'] as Map<String, dynamic>? ?? {};
return SeasonPalette(
deserialized.map(
(key, value) => MapEntry(
Season.fromJson(int.parse(key)),
value as String,
),
),
);
}

final Map<Season, String> colors;

Map<String, dynamic> toJson() {
return <String, dynamic>{
'colors': colors.map<int, String>(
(key, value) => MapEntry(key.toJson(), value),
),
};
}

@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
if (other is! SeasonPalette) return false;
if (colors.length != other.colors.length) return false;
for (final entry in colors.entries) {
if (other.colors[entry.key] != entry.value) return false;
}
return true;
}

@override
int get hashCode => colors.hashCode;
}

@immutable
class Season {
const Season._(this.index, this.name);

static const spring = Season._(0, 'spring');
static const summer = Season._(1, 'summer');
static const autumn = Season._(2, 'autumn');
static const winter = Season._(3, 'winter');

static const values = [spring, summer, autumn, winter];

final int index;
final String name;

int toJson() => index;

static Season fromJson(int value) {
return values.firstWhere((e) => e.index == value);
}

@override
bool operator ==(Object other) {
return identical(this, other) || other is Season && other.index == index;
}

@override
int get hashCode => index.hashCode;

@override
String toString() => 'Season.$name';
}
18 changes: 18 additions & 0 deletions packages/hydrated_bloc/test/e2e_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,24 @@ void main() {
});
});

group('SeasonPaletteCubit', () {
test(
'persists and restores state '
'when serialized state uses non-string keys', () async {
final palette = SeasonPalette({
Season.spring: 'green',
Season.summer: 'yellow',
Season.autumn: 'orange',
Season.winter: 'white',
});
final cubit = SeasonPaletteCubit();
expect(cubit.state, const SeasonPalette({}));
cubit.update(palette);
await sleep();
expect(SeasonPaletteCubit().state, palette);
});
});

group('FromJsonStateCubit', () {
test('does not throw StackOverflow ', () async {
final fromJsonCalls = <int>[];
Expand Down
37 changes: 37 additions & 0 deletions packages/hydrated_bloc/test/hydrated_cubit_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,21 @@ class MyMultiHydratedCubit extends HydratedCubit<int> {
int? fromJson(dynamic json) => json['value'] as int?;
}

class MyIntKeyMapCubit extends HydratedCubit<Map<int, String>> {
MyIntKeyMapCubit() : super(const {});

@override
Map<String, dynamic> toJson(Map<int, String> state) {
return {'data': state};
}

@override
Map<int, String> fromJson(Map<String, dynamic> json) {
final raw = json['data'] as Map<String, dynamic>? ?? {};
return raw.map((key, value) => MapEntry(int.parse(key), value as String));
}
}

class MyHydratedCubitWithCustomStorage extends HydratedCubit<int> {
MyHydratedCubitWithCustomStorage(Storage storage)
: super(0, storage: storage);
Expand Down Expand Up @@ -396,6 +411,28 @@ void main() {
});
});

group('MyIntKeyMapCubit', () {
test('serializes non-string keys', () {
final cubit = MyIntKeyMapCubit();
const data = {0: 'a', 1: 'b', 2: 'c'};
const change = Change(currentState: <int, String>{}, nextState: data);
cubit.onChange(change);
verify(
() => storage.write('MyIntKeyMapCubit', {
'data': {'0': 'a', '1': 'b', '2': 'c'},
}),
).called(1);
});

test('restores state when cache has stringified int keys', () {
when<dynamic>(() => storage.read(any())).thenReturn({
'data': {'0': 'a', '1': 'b'},
});
final cubit = MyIntKeyMapCubit();
expect(cubit.state, {0: 'a', 1: 'b'});
});
});

group('MyHydratedCubitWithCustomStorage', () {
setUp(() {
HydratedBloc.storage = null;
Expand Down
Loading