brianegan/flutter_redux

Socket and Flutter_redux

barilki opened this issue · 18 comments

Hi,
I'm using signal_core and flutter redux.
I have a problem with the dispatch inside the socket service.
Each time I get changes from socket and dispatch them, all the pages are rebuilt.
How can I prevent this from happening?

You need to use a StoreConnector Widget + the set distinct to true.

You can read more about it from the docs:

Every time the store changes, the Widget will be rebuilt. As a performance optimization, the Widget can be rebuilt only when the ViewModel changes. In order for this to work correctly, you must implement == and hashCode for the ViewModel, and set the distinct option to true when creating your StoreConnector.

And see an example here:

https://github.com/brianegan/flutter_architecture_samples/blob/master/redux/lib/containers/stats.dart

@brianegan I've been implement it and still the pages get rebuilt each time.

Every time I dispatch an action from the socket service, all my pages are rebuilt. i'm using distinct and == operator inside view model.

Could you share the code that isn't working, or at least a small reproducible sample? I've shared a working example above. Is something similar not working?

Distinct + == are tested, but there could still be a bug. Impossible for me to help without seeing some code.

https://github.com/brianegan/flutter_redux/blob/master/test/flutter_redux_test.dart#L626

For example:

setting page with setting view model

class NotificationsPageVM {
NotificationsPageVM({
this.notifyList,
this.notifyListFromServer,
this.allInstruments,
this.removeFromNotif,
this.getAlertsList,
this.notifications,
});

final List? notifyList;
final List? allInstruments;
final List<GetAlertsRespDto?>? notifyListFromServer;

final List? notifications;

final void Function(String id)? removeFromNotif;
final void Function()? getAlertsList;

static NotificationsPageVM fromStore(Store store) {
return NotificationsPageVM(
notifyList: store.state.notificationState.activeNotification,
allInstruments: store.state.symbolState.allSymbols,
notifyListFromServer: store.state.notificationState.activeNotificationList,
removeFromNotif: NotificationSelector.getRemoveFromNotificationsFunction(store),
getAlertsList: NotificationSelector.getAlertsList(store),
notifications: NotificationSelector.getNotificationsList(store),
);
}

@OverRide
bool operator ==(Object other) =>
identical(this, other) ||
other is NotificationsPageVM &&
runtimeType == other.runtimeType &&
notifyList == other.notifyList &&
notifyListFromServer == other.notifyListFromServer &&
allInstruments == other.allInstruments &&
removeFromNotif == other.removeFromNotif &&
getAlertsList == other.getAlertsList;

@OverRide
int get hashCode => notifyList.hashCode ^ notifyListFromServer.hashCode ^ allInstruments.hashCode ^ removeFromNotif.hashCode ^ getAlertsList.hashCode;
}

this is the small part of instrument page (Storeconnector)
@OverRide
Widget build(BuildContext context) {
return MainLayout(
key: Key('[NotificationsPage][MainLayout]'),
buildPhoneLayout: (BuildContext context, bool isSmallPhone) {
return StoreConnector<AppState, NotificationsPageVM>(
distinct: true,
converter: NotificationsPageVM.fromStore,
builder: (BuildContext context, NotificationsPageVM vm) {
....
}

in the main i dispatch event inside timer every second
Timer.periodic(
Duration(milliseconds: SocketConsts.INSTRUMENTS_TIMER_DELAY_IN_MILLISECONDS),
(_) {
widget.store.dispatch(UpdateSymbolsWithSocketDataAction(
symbols: symbols,
));
},
);

As a result, the setting page and other pages are always rebuilt.

Thanks. Do you have tests that verify the == method on the NotificationsPageVM is working as expected?

I could see a couple of issues here.

  1. By default, Dart does not compare List objects by content, but by identity. Therefore, if you're creating a new notifyList, allInstruments, notifyListFromServer or notifications list inside your reducer, the == method will assume these are two different objects. Many folks use the collection package's ListEquality class to compare the content of two lists.
  2. Dart also compares Function objects only by identity. It looks like your callback functions are created by a selctor, so I assume that's working as intended, but it'd still be good to verify with a test case.

First, I'd start by trying the following test to make sure the == method is working as expected. If the following test works, please let me know and we can dig deeper. However, this feels like something is going on with the == method, since that's all the distinct option does: compare the old ViewModel to the new ViewModel using the == operator.

test('NotificationsPageVM == works as expected', () {
  final store = createStore(); // Example function, should be replaced by how you're building your store
  final viewModel = NotificationsPageVM.fromStore(store);

  store.dispatch(UpdateSymbolsWithSocketDataAction(symbols: symbols));

  final newViewModel = NotificationsPageVM.fromStore(store);

  expect(viewModel.notifyList, equals(newViewModel.notifyList));
  expect(viewModel.allInstruments, equals(newViewModel.allInstruments));
  expect(viewModel.notifyListFromServer, equals(newViewModel.notifyListFromServer));
  expect(viewModel.notifications, equals(newViewModel.notifications));
  expect(viewModel.removeFromNotif, equals(newViewModel.removeFromNotif));
  expect(viewModel.getAlertsList, equals(newViewModel.getAlertsList));
  expect(viewModel, equals(newViewModel));
});

Looking at your code for a little longer, is the symbols List dispatched by the Timer.periodic( always a new List? If so, I'd imagine that might be the root of the problem.

class NotificationsPageVM{
NotificationsPageVM({
this.notifyList,
this.notifyListFromServer,
this.allInstruments,
this.removeFromNotif,
this.getAlertsList,
this.notifications,
});

final List? notifyList;
final List? allInstruments;
final List<GetAlertsRespDto?>? notifyListFromServer;

final List? notifications;

final void Function(String id)? removeFromNotif;
final void Function()? getAlertsList;

static NotificationsPageVM fromStore(Store store) {
return NotificationsPageVM(
notifyList: store.state.notificationState.activeNotification,
allInstruments: store.state.symbolState.allSymbols,
notifyListFromServer: store.state.notificationState.activeNotificationList,
removeFromNotif: NotificationSelector.getRemoveFromNotificationsFunction(store),
getAlertsList: NotificationSelector.getAlertsList(store),
notifications: NotificationSelector.getNotificationsList(store),
);
}

@OverRide
bool operator ==(Object other) {
if (identical(this, other)) return true;

return other is NotificationsPageVM &&
    listEquals(other.notifyList, notifyList) &&
    listEquals(other.notifyListFromServer, notifyListFromServer) &&
    listEquals(other.allInstruments, allInstruments) &&
    listEquals(other.notifications, notifications) &&
    other.removeFromNotif == removeFromNotif &&
    other.getAlertsList == getAlertsList;

}

@OverRide
int get hashCode {
return notifyList.hashCode ^
notifyListFromServer.hashCode ^
allInstruments.hashCode ^
notifications.hashCode ^
removeFromNotif.hashCode ^
getAlertsList.hashCode;
}
}

Still the same, notification page are always rebuilt

Yes test case passed

Your assistance would greatly be appreciated. I need it urgently, thanks.

  1. widget isn't rebuild outside the builder, only inside store connector builder.
  2. == method return true.
  3. bool _whereDistinct(ViewModel vm) {
    if (widget.distinct) ==> always true
    {
    return vm != _latestValue; ==> always false
    }
    return true;
    }
  4. I'm not quite understanding

I/flutter ( 9789): _whereDistinct: false
I/flutter ( 9789): _whereDistinct: true
I/flutter ( 9789): _handleChange: true
I/flutter ( 9789): _whereDistinct: false
I/flutter ( 9789): _whereDistinct: false
I/flutter ( 9789): builder: Notification Page VM

this is what it get.

Thanks. This is what I'd expect if the Stream is working as expected. If two view models are equal using the == method, _whereDistinct correctly returns false and the Stream does not continue / the widget is not rebuilt.

However, from the pasted log, you can see that in some cases, your == method shows that two view models are not equal, so _whereDistinct returns true. This will cause the widget to rebuild, since it detects a new viewmodel has been emitted.

Please examine your == method when it returns false to figure out the root cause of the problem. In order to prevent the widget from rebuilding, _whereDistinct must return false.

allInstruments return false inside == operator

In that case, you've found the root of the issue! The allInstruments instance variable content changes.

If the list content changes, the two view models are no longer equal, and flutter_redux will rebuild the widget tree using the builder function, passing in new view model.

To prevent a whole page from rebuilding, the best thing to do would be to remove the allInstruments variable from your view model, and wrap only the specific part of the page that needs to rebuild when allInstruments changes with a StoreConnector widget that extracts that info.