River_pod: Allow to dispose maintainState Providers

Created on 18 Sep 2020  路  10Comments  路  Source: rrousselGit/river_pod

Is your feature request related to a problem? Please describe.
maintainState is a very nice feature for example for list pagination. But the problem is that maybe in one Flow we have 3 different screens withs lists, and then the user go to other Flow and continue using the app and never go back to this lists. So we have on memory different Providers states (a lot really, especially if we use Provider.family with "page" as param like Marvel example list) that we didn't need.

Describe the solution you'd like
I think on two solutions.

  1. An autodispose timer for maintainState Providers. Like
final characterPages = FutureProvider.autoDispose.family<MarvelListCharactersReponse, CharacterPagination>(
  (ref, meta) async {
    final cancelToken = CancelToken();
    ref.onDispose(cancelToken.cancel);

    final repository = ref.read(repositoryProvider);
    final charactersResponse = await repository.fetchCharacters(
      offset: meta.page * kCharactersPageLimit,
      limit: kCharactersPageLimit,
      nameStartsWith: meta.name,
      cancelToken: cancelToken,
    );

    ref.maintainStateUntil(Duration(seconds: 60); // for not use two different properties assignment
    return charactersResponse;
  },
);
  1. A way for manual dispose named Providers/family Providers.
final characterPages = FutureProvider.autoDispose.family<MarvelListCharactersReponse, CharacterPagination>(
  (ref, meta) async {
    final cancelToken = CancelToken();
    ref.onDispose(cancelToken.cancel);

    final repository = ref.read(repositoryProvider);
    final charactersResponse = await repository.fetchCharacters(
      offset: meta.page * kCharactersPageLimit,
      limit: kCharactersPageLimit,
      nameStartsWith: meta.name,
      cancelToken: cancelToken,
    );

    ref.maintainState = true;
    return charactersResponse;
  }, name: 'marvelCharacters'
);

ProviderContainer container = ProviderScope.containerOf(context);
container.disposeStatesFromNamed("marvelCharacters") //this dispose all states associated to this family provider
enhancement

Most helpful comment

container.disposeStatesFromNamed("marvelCharacters") //this dispose all states associated to this family provider

What about:

final family = Provider.family(...);
final provider = Provider(...);

context.disposeFamily(family);
context.disposeProvider(provder);

All 10 comments

I think add a way to set duration is a good idea :D

In the example you've described, chances that a timer likely wouldn't work.

If the ListView is still in the widget tree (and chances are it is, as routes that are not visible are still mounted), the state would be preserved anyway even without maintainState = true

container.disposeStatesFromNamed("marvelCharacters") //this dispose all states associated to this family provider

What about:

final family = Provider.family(...);
final provider = Provider(...);

context.disposeFamily(family);
context.disposeProvider(provder);

In the example you've described, chances that a timer likely wouldn't work.

If the ListView is still in the widget tree (and chances are it is, as routes that are not visible are still mounted), the state would be preserved anyway even without maintainState = true

Well, I'm using SliverList with the ProviderScope "override" approach (like the Marvel Grid example). So when I scroll down and go back, if I not maintein the state, I lose it. I still agree that "Duration" is not complete ideal in this scenario.

container.disposeStatesFromNamed("marvelCharacters") //this dispose all states associated to this family provider

What about:

final family = Provider.family(...);
final provider = Provider(...);

context.disposeFamily(family);
context.disposeProvider(provder);

Totally! Even better.

Well, I'm using SliverList with the ProviderScope "override" approach (like the Marvel Grid example). So when I scroll down and go back, if I not maintein the state, I lose it. I still agree that "Duration" is not complete ideal in this scenario.

Indeed. But the currently visible item wouldn't be disposed

Well, I'm using SliverList with the ProviderScope "override" approach (like the Marvel Grid example). So when I scroll down and go back, if I not maintein the state, I lose it. I still agree that "Duration" is not complete ideal in this scenario.

Indeed. But the currently visible item wouldn't be disposed

Sure. With maintainState I can guarantee a correct UX when I'm on the SliverList screen doing down/up gestures. I really need maintain the states here, then when I leave the screen the only important states to maintein are the used on the visible items until the screen was removed from Navigation stack.
I supose with your solution I can do something like:

useEffect(() {
    ...
    return () {
      context.disposeFamily(characterPages);
    };
  });

On the List screen HookWidget. With this I can maintain all the states for the family for don't lose the UX effect and avoid unnecessary server requests. The disposeFamily feature is important to if I need pull to request, because maybe one thing change and I need refresh the data.

I supose with your solution I can do something like:

useEffect(() {
   ...
   return () {
     context.disposeFamily(characterPages);
   };
 });

Sadly no, it's more complex.
We don't want to dispose the provider when the widget is disposed. The issue is that the widget isn't disposed, so this would have no impact.

What you want is likely something on Navigator's level instead, such that the maintainStateFor(Duration) you are suggesting would be implemented for Navigator routes.
Then, when routes that are no-longer visible are destroyed, this would automatically dispose the providers.

But the maintainStateFor(Duration) for providers may be interesting for ListViews, to not keep items that haven't been viewed for a while alive.
Although tbh, this could be moved to the Widget layer too.

Rather than using maintainState = true, you may want to not use the flag at all, and instead increase the ListView.cacheExtent variable:

ListView(
    cacheExtent: 500?,
)

This would keep items that are not visible but close to being visible still alive. So if the user scrolls very far, items are disposed of. But if the item barely left the screen, its state is preserved.

The disposeFamily feature is important to if I need pull to request, because maybe one thing change and I need refresh the data.

For refreshing a group of providers, there's an easy solution: create a dummy provider that all providers of this group listen to with ref.watch

final group = Provider<void>((ref) {});

final a = Provider((ref) {
  ref.watch(group);
  print('create A');
});

final B = Provider((ref) {
  ref.watch(group);
  print('create B');
});

....

context.refresh(group); // Will cause `a` and `b` to refresh

Sadly no, it's more complex.
We don't want to dispose the provider when the widget is disposed. The issue is that the widget isn't disposed, so this would have no impact.

What you want is likely something on Navigator's level instead, such that the maintainStateFor(Duration) you are suggesting would be implemented for Navigator routes.
Then, when routes that are no-longer visible are destroyed, this would automatically dispose the providers.

But the maintainStateFor(Duration) for providers may be interesting for ListViews, to not keep items that haven't been viewed for a while alive.
Although tbh, this could be moved to the Widget layer too.

You are right. Needs to be worked at the navigation layer.
I think the two "ideas" (maintainStateFor and context.disposeFamily / Provider) are not mutually exclusive and give us more versatility for manage with more criterias Providers State lifes: On the one hand is desider a duration (or other criteria) for allow maintainState autodispose himself (I never really like the idea for `maintainState' never expire) and on the other give us more control to ensure at some point in the app some Providers/Family States were disposed.

Rather than using maintainState = true, you may want to not use the flag at all, and instead increase the ListView.cacheExtent variable:

ListView(
    cacheExtent: 500?,
)

This would keep items that are not visible but close to being visible still alive. So if the user scrolls very far, items are disposed of. But if the item barely left the screen, its state is preserved.

I test it but the behiever it's not what I expect (for the last thing you comment and for making the user fetch/render much more data/widget than he needs).

The disposeFamily feature is important to if I need pull to request, because maybe one thing change and I need refresh the data.

For refreshing a group of providers, there's an easy solution: create a dummy provider that all providers of this group listen to with ref.watch

final group = Provider<void>((ref) {});

final a = Provider((ref) {
  ref.watch(group);
  print('create A');
});

final B = Provider((ref) {
  ref.watch(group);
  print('create B');
});

....

context.refresh(group); // Will cause `a` and `b` to refresh

But if Providers "a" and "b" are using maintainState = true, this works?

Was this page helpful?
0 / 5 - 0 ratings