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

Possible Bug: BaseModel.equals not working with null property #40

Closed ghost closed 4 years ago

ghost commented 4 years ago

I got the following case happening in my application. Whenever the currentTrip property of my state mutates, i get the desired results, my widget is rebuilt to show new information. The way this variable mutates is through the following action:

class FetchTripAction extends ReduxAction<AppState>{
  @override
  FutureOr<AppState> reduce() async {
    Trip trip;

    // -- Fetch current trip from server
    ApiResponse tripResponse = await ApiServices.get( "/api/current-trip" );

    // -- If response contains trip data, parse it
    if( JsonUtils.isJson( tripResponse.data ) ) {
      // -- Parse trip from received data
      trip = Trip.fromJson( tripResponse.data );
    }

    // -- Create new state. Trip CAN be null if employee is not in trip
    return state.copy(
      currentTrip: trip
    );
  }
}

The problem occurs whenever currentTrip mutates into a null value. My widget is not being rebuilt with new information. It seems to only be the case whenever it is null, if a new trip is fetched from the server with different information, my widget is rebuilt as desired. Is there anything that I am overlooking? Thank you dearly in advance !

This is the class that contains current state of my app

class AppState{
  // -- App vars
  final bool working;
  final bool tripInProgress;
  final bool nearVisitLocation;
  final bool websocketConnected;
  final Position currentPosition;
  final List<BranchPhone> branchPhones;

  // -- Entities
  final Trip currentTrip;
  final Employee deliveryEmployee;

  AppState({this.working, this.currentPosition, this.nearVisitLocation, this.tripInProgress,
    this.deliveryEmployee, this.currentTrip, this.websocketConnected, this.branchPhones});

  AppState copy({bool working, Position currentPosition, List<String> savedUsernames,
    bool nearVisitLocation, bool tripInProgress, Employee deliveryEmployee, Trip currentTrip,
    bool websocketConnected, List<BranchPhone> branchPhones,}) => AppState(
        // --
    branchPhones: branchPhones ?? this.branchPhones,
    currentPosition: currentPosition ?? this.currentPosition,
    currentTrip: currentTrip ?? this.currentTrip,
    deliveryEmployee: deliveryEmployee ?? this.deliveryEmployee,
    nearVisitLocation: nearVisitLocation ?? this.nearVisitLocation,
    tripInProgress: tripInProgress ?? this.tripInProgress,
    websocketConnected: websocketConnected ?? this.websocketConnected,
    working: working ?? this.working,
  );

  static Future<AppState> initialState() async{
    // -- Fetch employee if previously logged in and is persisted
    Employee currentEmployee = await Prefs.getFromJson( Prefs.kEmployeeJson , Employee.fromJson );

    // -- Fetch if currently in trip
    bool tripInProgress = StringUtils.parseBoolean( await Prefs.getString( Prefs.kTripInProgress ) );

    // -- Return app state
    return AppState(
      currentPosition: null,
      currentTrip: null,
      branchPhones: [],
      deliveryEmployee: currentEmployee,
      nearVisitLocation: false,
      tripInProgress: tripInProgress,
      websocketConnected: false,
      working: false,
    );
  }
}

The following is the connector widget

class HomePageConnector extends StatelessWidget{
  @override
  Widget build(BuildContext context) {
    return StoreConnector<AppState, HomeViewModel>(
      model: HomeViewModel(),
      onInit: ( Store<AppState> state ) => Hermes.connect(),
      builder: ( BuildContext context, HomeViewModel model ) => HomePage(
        currentTrip: model.currentTrip,
        employee: model.employee,
        branchPhonesCallback: model.branchPhonesCallback,
        logoutCallback: model.logoutCallback,
        startTripCallback: model.startTripCallback,
        websocketConnected: model.websocketConnected,
        working: model.working,
      ),
    );
  }
}

class HomeViewModel extends BaseModel<AppState>{
  // -- Functions and callbacks to pass to view
  Function branchPhonesCallback;
  Function logoutCallback;
  Function startTripCallback;

  // -- Entities and app state to pass to view
  bool working;
  Trip currentTrip;
  Employee employee;
  bool websocketConnected;

  HomeViewModel();
  HomeViewModel.build({
    @required this.branchPhonesCallback,
    @required this.logoutCallback,
    @required this.startTripCallback,
    @required this.currentTrip,
    @required this.employee,
    @required this.websocketConnected,
    @required this.working,
  }) : super(equals: [ currentTrip , employee, websocketConnected, working ]);

  @override
  HomeViewModel fromStore() {
    return HomeViewModel.build(
      branchPhonesCallback: () => dispatch( FetchBranchPhonesAction( employee: store.state.deliveryEmployee ) ),
      logoutCallback: () => dispatch( LogoutAction() ),
      startTripCallback: () => dispatch( StartTripAction( currentTrip: store.state.currentTrip ) ),
      currentTrip: store.state.currentTrip,
      employee: store.state.deliveryEmployee,
      websocketConnected: store.state.websocketConnected,
      working: store.state.working
    );
  }
}
ghost commented 4 years ago

Found the culprit ! Working in late hours of the night really makes one overlook things, whoops ! The issue is the following:

AppState copy({bool working, Position currentPosition, List<String> savedUsernames,
    bool nearVisitLocation, bool tripInProgress, Employee deliveryEmployee, Trip currentTrip,
    bool websocketConnected, List<BranchPhone> branchPhones,}) => AppState(
        // --
    currentTrip: currentTrip ?? this.currentTrip, //<== This is the problem
  );

I cant believe i overlooked this. Closing my issues, thank you regardless !