Cretezy / redux_persist

Persist Redux State
https://pub.dartlang.org/packages/redux_persist
MIT License
130 stars 41 forks source link

SerializationException: Save: Converting object to an encodable object failed #12

Closed nicolashemonic closed 6 years ago

nicolashemonic commented 6 years ago

Hi,

I installed latest version:

redux:
flutter_redux:
redux_thunk:
redux_persist:
redux_persist_flutter:

When an action is dispatched the following exception is thrown:

SerializationException: Save: Converting object to an encodable object failed: Instance of 'AppState'

[VERBOSE-2:dart_error.cc(16)] Unhandled exception:
SerializationException: Save: Converting object to an encodable object failed: Instance of 'AppState'
#0      Persistor.save (package:redux_persist/redux_persist.dart:222:7)
<asynchronous suspension>
#1      Persistor.createMiddleware.<anonymous closure> (package:redux_persist/redux_persist.dart:84:13)
#2      Store._createDispatchers.<anonymous closure> (package:redux/src/store.dart:238:43)
#3      Store.dispatch (package:redux/src/store.dart:250:20)
#4      LogInForm.build.<anonymous closure>.<anonymous closure> (file:///Users/nicolas/Projects/Availpro/mobile-starter-kit/lib/containers/log_in_form.dart:14:21)
#5      LogInFormState.onFormSubmit (file:///Users/nicolas/Projects/Availpro/mobile-starter-kit/lib/widgets/log_in_form.dart:22:20)
#6      LogInFormState.buildFormSubmitButton.<anonymous closure> (file:///Users/nicolas/Projects/Availpro/mobile-starter-kit/lib/widgets/log_in_form.dart:86:28)
#7      _InkResponseState._handleTap (package:flutte<…>

Find below redux_persist implementation.

Main

void main() {
  final persistor = new Persistor<AppState>(
      storage: new FlutterStorage("mobile-starter-kit"),
      decoder: AppState.fromJson);

  final store = new Store<AppState>(appReducer,
      initialState: new AppState(
        counter: new CounterState(),
        authentication: new AuthenticationState(),
      ),
      middleware: [persistor.createMiddleware(), thunkMiddleware]);

  persistor.load(store);
  runApp(App(store: store));
}

App State

class AppState {
  final CounterState counter;
  final AuthenticationState authentication;

  const AppState({@required this.counter, @required this.authentication});

  static AppState fromJson(dynamic json) => new AppState(
      counter: json['counter'], authentication: json['authentication']);

  dynamic toJson() => {'counter': counter, 'authentication': authentication};
}

App Reducer

AppState appReducer(AppState state, action) {
  if (action is PersistLoadedAction<AppState>) {
    return action.state ?? state;
  }
  return new AppState(
      counter: counterReducer(
        state.counter,
        action,
      ),
      authentication: authenticationReducer(state.authentication, action));
}

Thanks for your help !

nicolashemonic commented 6 years ago

The exception is thrown by Dart:convert json.encode in redux_persist.dart.

try {
  // Encode to JSON
  // TODO: add custom serializer
  transformedJson = json.encode(versionedState);
} catch (error) {
  throw new SerializationException('Save: ${error.toString()}');
}

Following Dart documentation, if value contains objects that are not directly encodable to a JSON string (a value that is not a number, boolean, string, null, list or a map with string keys), the toEncodable function is used to convert it to an object that must be directly encodable.

If toEncodable is omitted, it defaults to a function that returns the result of calling .toJson() on the unencodable object.

The solution to resolve JSON encode exception:

Each object model (that are not map with string keys) must implement .toJson(). My state object models are now like this counter state:

class CounterState {
  final int value;

  const CounterState({this.value = 0});

  CounterState copyWith({int value}) {
    return new CounterState(value: value ?? this.value);
  }

  // ! Add toJson serializer
  dynamic toJson() => {'value': value};
}

For more details, Flutter JSON Serialization documentation can help.

nicolashemonic commented 6 years ago

@Cretezy I think it would be nice to have more documentation about this and save lot of time of everyone. 😉

nicolashemonic commented 6 years ago

Maintaining serialization could be complex when app growth. For anyone search a better solution than writing toJson() manually you can find below an example of how to implement JSON Serializable library with redux_persist.

pubspec.yaml

dependencies:
    json_serializable:

dev_dependencies:
    build_runner:

app_state.dart

import 'package:json_annotation/json_annotation.dart';
import 'counter_state.dart';
part 'app_state.g.dart';

@JsonSerializable()
class AppState extends Object with _$AppStateSerializerMixin {
  final CounterState counter;

  AppState({this.counter});

  factory AppState.fromJson(dynamic json) => _$AppStateFromJson(json);

  static AppState fromJsonDecoder(dynamic json) => AppState.fromJson(json);
}

counter_state.dart

import 'package:json_annotation/json_annotation.dart';
part 'counter_state.g.dart';

@JsonSerializable()
class CounterState extends Object with _$CounterStateSerializerMixin {
  final int value;

  CounterState({this.value = 0});

  CounterState copyWith({int value}) {
    return new CounterState(value: value ?? this.value);
  }

  factory CounterState.fromJson(dynamic json) => _$CounterStateFromJson(json);
}

app.dart

final persistor = new Persistor<AppState>(
    storage: new FlutterStorage("mobile-starter-kit"),
    decoder: AppState.fromJsonDecoder);

final store = new Store<AppState>(appReducer,
    initialState: new AppState(
      counter: new CounterState()
    ),
    middleware: [persistor.createMiddleware()]);

persistor.load(store);

Then, you can generates json serialization code using flutter packages pub run build_runner build command before flutter run.

More details in flutter doc.

Cretezy commented 6 years ago

It definitely needs a better way to serialize things. I was planning on adding module serializers.

I'll try to get to it this weekend, been very busy recently. Let me know of your ideas on implementations

Cretezy commented 6 years ago

This was added in 0.8.0-rc.0, let me know if that fixes your problem!