marcglasberg / async_redux

Flutter Package: A Redux version tailored for Flutter, which is easy to learn, to use, to test, and has no boilerplate. Allows for both sync and async reducers.
Other
234 stars 40 forks source link

How to handle state upgrades (versioning) in the persistor? #88

Closed mohammadameer closed 4 years ago

mohammadameer commented 4 years ago

in our app, we are using shared Preferences to store the last state and use it again

the problem is that when we add new properties to the state new properties values are null because of how we handle old state how we can fix that and what is the best way

AppPersistor.dart

class AppPersistor extends Persistor<AppState> {
  Future<SharedPreferences> _prefs = SharedPreferences.getInstance();
  final LocalPersist fileHandler = LocalPersist("appState");

  @override
  Future<void> deleteState() async {
    final SharedPreferences prefs = await _prefs;
    prefs.clear();
  }

  @override
  Future<void> persistDifference(
      {AppState lastPersistedState, AppState newState}) async {
    final SharedPreferences prefs = await _prefs;
    await deleteState();
    prefs.setString("AppState", json.encode(newState.toJson()));
  }

  @override
  Future<AppState> readState() async {
    final SharedPreferences prefs = await _prefs;

    String obj = prefs.get("AppState");
    if (obj == null) return null;
    return AppState.fromJson(json.decode(obj));
  }
}

main.dart

...

 AppPersistor persistor = AppPersistor();
  var state = await persistor.readState();
  if (state == null) {
    state = AppState.initialState();
    await persistor.saveInitialState(state);
  }
  Store<AppState> store = Store<AppState>(
      initialState: state,
      persistor: persistor,
      modelObserver: DefaultModelObserver());

...
marcglasberg commented 4 years ago

You need to explain it better for me to understand what's the problem you're having, exactly. Could you be more specific, and give me more details? What are you trying to do, and what is not working?

mohammadameer commented 4 years ago

if you see this code

AppPersistor persistor = AppPersistor();

  var state = await persistor.readState(); // get old state

  if (state == null) { // if there is no old state

    state = AppState.initialState(); // initialize the state 

    await persistor.saveInitialState(state);  //  save the initialized state

  }

  Store<AppState> store = Store<AppState>(
      initialState: state,
      persistor: persistor,
      modelObserver: DefaultModelObserver());

we are getting the old state if there is no state we will initialize the state and save it

the problem is when we add a new property to the state it value not be initialized because there is old state and it's value will be null

marcglasberg commented 4 years ago

So, you evolved your state that now has new information, and you want to do a "database migration" from the old state schema to the new one.

Basically you have to detect you don't have the old state, and then create it.

You have two options:

1) Detect that some state that should not be null was read as null, and then instantiate it directly:

var state = await persistor.readState(); // get old state
if (state == null) { // if there is no old state
    state = AppState.initialState(); // initialize the state 
    await persistor.saveInitialState(state);  //  save the initialized state
  }
else {
  if (state.myNewInfo == null) state = state.copy(myNewInfo: MyNewInfo.initialState());
  await persistor.saveInitialState(state);  //  save the initialized state
}

2) Save a version number in the state, and upgrade it explicitly as needed:

static const version = "1.2";

var state = await persistor.readState(); // get old state
if (state == null) { // if there is no old state
    state = AppState.initialState(); // initialize the state 
    await persistor.saveInitialState(state);  //  save the initialized state
  }
else {
  if (state.version != version) state = state.copy(version: version, myNewInfo: MyNewInfo.initialState());
  await persistor.saveInitialState(state);  //  save the initialized state
}
mohammadameer commented 4 years ago

I have found that we can use @JsonKey(defaultValue: ...) to give the field value if it was null when serialization fromJson but when I want to give a subState a default value I get this error

image

this is my UserState.dart

import 'package:json_annotation/json_annotation.dart';
import 'package:saree3/models/Notification.dart';
import 'package:saree3/store/DelivererState.dart';

part 'UserState.g.dart';

@JsonSerializable(explicitToJson: true, nullable: true)
class UserState {
  final String id;
  final String token;
  final String registrationToken;
  final String fullName;
  final String phone;
  final String code;
  final num walletBalance;
  final String walletId;
  final bool isDeliverer;
  final String vehicleType;
  final String vehicleProof;
  final String cardID;
  final String imageID;
  final String location;
  final DelivererState delivererState;
  final bool newNotification;
  final List<Notification> notifications;

  UserState(
      {this.id,
      this.token,
      this.registrationToken,
      this.fullName,
      this.phone,
      this.code,
      this.walletBalance,
      this.walletId,
      this.isDeliverer,
      this.vehicleType,
      this.vehicleProof,
      this.cardID,
      this.imageID,
      this.location,
      this.delivererState,
      this.newNotification,
      this.notifications});

  UserState copy(
          {String id,
          String token,
          String registrationToken,
          String fullName,
          String phone,
          String code,
          num walletBalance,
          String walletId,
          bool isDeliverer,
          String vehicleType,
          String vehicleProof,
          String cardID,
          String imageID,
          String location,
          DelivererState delivererState,
          bool newNotification,
          List<Notification> notifications}) =>
      UserState(
          id: id ?? this.id,
          token: token ?? this.token,
          registrationToken: registrationToken ?? this.registrationToken,
          fullName: fullName ?? this.fullName,
          phone: phone ?? this.phone,
          code: code ?? this.code,
          walletBalance: walletBalance ?? this.walletBalance,
          walletId: walletId ?? this.walletId,
          isDeliverer: isDeliverer ?? this.isDeliverer,
          vehicleType: vehicleType ?? this.vehicleType,
          vehicleProof: vehicleProof ?? this.vehicleProof,
          cardID: cardID ?? this.cardID,
          imageID: imageID ?? this.imageID,
          location: location ?? this.location,
          delivererState: delivererState ?? this.delivererState,
          newNotification: newNotification ?? this.newNotification,
          notifications: notifications ?? this.notifications);

  static UserState initialState() => UserState(
      id: "",
      token: "",
      registrationToken: "",
      fullName: "",
      phone: "",
      code: "",
      isDeliverer: false,
      vehicleType: "",
      vehicleProof: "",
      cardID: "",
      imageID: "",
      location: "",
      delivererState: DelivererState.initialState(),
      newNotification: false,
      notifications: List.unmodifiable(<Notification>[]));

  factory UserState.fromJson(Map<String, dynamic> json) =>
      _$UserStateFromJson(json);

  Map<String, dynamic> toJson() => _$UserStateToJson(this);

  @override
  bool operator ==(other) =>
      identical(this, other) ||
      other is UserState &&
          runtimeType == other.runtimeType &&
          id == other.id &&
          token == other.token &&
          registrationToken == other.registrationToken &&
          fullName == other.fullName &&
          phone == other.phone &&
          code == other.code &&
          isDeliverer == other.isDeliverer &&
          vehicleType == other.vehicleType &&
          vehicleProof == other.vehicleProof &&
          cardID == other.cardID &&
          imageID == other.imageID &&
          location == other.location &&
          delivererState == other.delivererState;

  @override
  int get hashCode =>
      id.hashCode ^
      token.hashCode ^
      hashCode ^
      fullName.hashCode ^
      phone.hashCode ^
      code.hashCode ^
      isDeliverer.hashCode ^
      vehicleType.hashCode ^
      vehicleProof.hashCode ^
      cardID.hashCode ^
      imageID.hashCode ^
      location.hashCode ^
      delivererState.hashCode;
}
marcglasberg commented 4 years ago

Your error message says you can only define a default value like this if it is constant.

I guess it'll work if UserState.initialState is a const constructor (see https://flutterigniter.com/dart-const-constructor).

Something along the lines of:

const UserState.initialState() : this(
      id: "",
      token: "",
      registrationToken: "",
      fullName: "",
      phone: "",
      code: "",
      isDeliverer: false,
      vehicleType: "",
      vehicleProof: "",
      cardID: "",
      imageID: "",
      location: "",
      delivererState: null,
      newNotification: false,
      notifications: const <Notification>[],
);

and then:

@JsonKey(defaultValue: const UserState.initialState())