riverpod icon indicating copy to clipboard operation
riverpod copied to clipboard

Disposing provider's state

Open KristianBalaj opened this issue 4 years ago • 20 comments

Situation description

.autodispose provider is being disposed when no more widgets are subscribed to it.

Having this kind of widget tree: MyScaffold widget at the top with the bottom navigation, swaping body widgets when clicked to the bottom navigation.

Let's have 2 body widgets - ProductsListView and HomeView. There is a productsStateProvider (.autoDispose) that stores all the fetched products in the ProductsListView.

Problem description

productsStateProvider is subscribed in the ProductsListView and preserves state like it should. But the problem is when navigating to the HomeView widget. The productsStateProvider is disposed.

I want to preserve the state in the whole application (in both views - ProductsListView, HomeView and in MyScaffold, too) and dispose only when poping MyScaffold from the widgets tree.

Possible solution (unwanted)

Possible solution would be to add watch/useprovider on the productsStateProvider inside the MyScaffold, but then I'm wasting rebuilds (rebuilding all under MyScaffold).

Is there any way how to solve this elegantly using riverpod/hooks?

KristianBalaj avatar Oct 12 '20 13:10 KristianBalaj

Have you tried using maintainState = true; inside the productsStateProvider

EdwynZN avatar Oct 13 '20 05:10 EdwynZN

@EdwynZN yeah it would solve the preservation of the state and the dispose would be done only on context.refresh.

But I want to dispose it when when popping/disposing the given widget.

I think, that one possible solution could be to use the ScopedProvider with overriding the provider in the widget tree. But then I can listen/watch only to changes on the whole provider and I cannot do select and listen only to given fields/situations.

And I cannot do read on this provider (for example inside the button callback).


Hmm, I think I'm getting it a bit. For select I could use the ProviderListener.

But what about the read. I will try to restructure the providers in the project somehow and I will see.

KristianBalaj avatar Oct 13 '20 11:10 KristianBalaj

Possible solution would be to add watch/useprovider on the productsStateProvider inside the MyScaffold, but then I'm wasting rebuilds (rebuilding all under MyScaffold).

That sounds like the correct solution

An easy way to fix the associated rebuild issue is to extract your scaffold in a widget with a const constructor:

watch(myProviderThatShouldStayAlive);

return const _MyScaffold();

This way, _MyScaffold will not rebuild

rrousselGit avatar Oct 13 '20 11:10 rrousselGit

@rrousselGit confirming this solution with const.

KristianBalaj avatar Oct 17 '20 19:10 KristianBalaj

Encountered the same use case. Would it make sense to expose an API to register "reference" on a autodispose provider? Using watch() without needing any value seems like a hack.

My use case is a bit different:

  • Widget A has a ListView with builder, which contains an item Widget B (so B is built after A)
  • On init, Widget A triggers a chain of reaction which ends pushing a value to StateProvider B
  • Widget B watches on StateProvider B
  • StateProvider B is autoDispose

Between the time that Widget A ends up pushing a value to StateProvider B, and when Widget B builds using StateProvider B, the value of StateProvider B is disposed. So when Widget B builds, it gets a null value from StateProvider B.

As suggested, it's solved by watching StateProvider B in Widget A, just to keep a reference on the provider.

lukaspili avatar Nov 19 '20 20:11 lukaspili

Would it make sense to expose an API to register "reference" on a autodispose provider?

How would that look like?

Using watch() without needing any value seems like a hack.

From my perspective, this isn't a hack.

But I do consider disallowing context.read on .autoDispose providers to prevent any mistake from happening

rrousselGit avatar Nov 19 '20 20:11 rrousselGit

In my case, it's another Provider (not .autodispose) which pushes a value to StateProvider B. The code looks like:

Data layer, shared by both widgets

final detailsProvider = StateProvider.autoDispose<Model>((_) => null);
final repositoryProvider = Provider((ref) => Repository._(ref.read));

class Repository {
  final Reader read;

  Future fetchModel() async {
    final response = await request ...
    response.when(
      success: (model, _) => read(_detailsProvider).state = model,
      ...
    );
  }
}

Widget A (builds a Widget B)

final _controllerProvider = StateNotifierProvider.autoDispose<_Controller>((ref) {
  // Theoretical, register a reference here:
  // It should not build a new _Controller when detailsProvider.state changes
  ref.dependsOn(detailsProvider);

  ref.onDispose(() {
    // detailsProvider gets disposed too
  });

  return _Controller(ref.read);
});

class _Controller extends StateNotifier<ScreenState> {
  _Controller(this.read) : super(ScreenState()) {
    fetch();
  }
  final Reader read;

  void fetch() async {
      await read(repositoryProvider).fetch();
      state = state.copy(fetchFinished: true);
  }
}

class WidgetA extends HookWidget {
  @override
  Widget build(BuildContext context) {
    final show = useProvider(_controllerProvider.state.select((s) => s.fetchFinished));

    // Conditionally builds WidgetB when fetch request finished
    return ConditionalBuilder(
      show: show,
      builder: (_) => const WidgetB(),
    );
  }
}

Widget B


class WidgetB extends HookWidget {
  @override
  Widget build(BuildContext context) {
    useProvider(detailsProvider).state // == null
  }
}

Sorry for the long example, tried to make it as short as possible.

lukaspili avatar Nov 19 '20 21:11 lukaspili

ref.dependsOn(detailsProvider)

Why not do:


ref.dependsOn(detailsProvider)

On Thu, 19 Nov 2020, 21:24 Lukasz Piliszczuk, [email protected] wrote:

In my case, it's another Provider (not .autodispose) which pushes a value to StateProvider B. The code looks like:

Data layer, shared by both widgets

final detailsProvider = StateProvider.autoDispose<Model>(() => null);final repositoryProvider = Provider((ref) => Repository.(ref.read)); class Repository { final Reader read;

Future fetchModel() async { final response = await request ... response.when( success: (model, _) => read(_detailsProvider).state = model, ... ); } }

Widget A (contains a ListView, including a Widget B)

final _controllerProvider = StateNotifierProvider.autoDispose<_Controller>((ref) { // Theoretical, register a reference here: // It should not build a new _Controller when detailsProvider.state changes ref.dependsOn(detailsProvider);

ref.onDispose(() { // detailsProvider gets disposed too });

return _Controller(ref.read); }); class _Controller extends StateNotifier<ScreenState> { _Controller(this.read) : super(ScreenState()) { fetch(); } final Reader read;

void fetch() async { await read(repositoryProvider).fetch(); state = state.copy(fetchFinished: true); } } class WidgetA extends HookWidget { @override Widget build(BuildContext context) { final show = useProvider(_controllerProvider.state.select((s) => s.fetchFinished));

// Conditionally builds WidgetB when fetch request finished
return ConditionalBuilder(
  show: show,
  builder: (_) => const WidgetB(),
);

} }

Widget B

class WidgetB extends HookWidget { @override Widget build(BuildContext context) { useProvider(detailsProvider).state // == null } }

Sorry for the long example, tried to make it as short as possible.

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/rrousselGit/river_pod/issues/185#issuecomment-730645683, or unsubscribe https://github.com/notifications/unsubscribe-auth/AEZ3I3ITPGE2LLPCB4CAUYDSQWEJBANCNFSM4SM2FYSA .

rrousselGit avatar Nov 19 '20 21:11 rrousselGit

What do you mean?

lukaspili avatar Nov 19 '20 22:11 lukaspili

Oh sorry the message wasn't sent properly

I meant instead of:

ref.dependsOn(detailsProvider)

do:

ref.watch(detailsProvider)

rrousselGit avatar Nov 19 '20 22:11 rrousselGit

The problem is that when a new value is pushed to detailsProvider, it will reset and build a new instance of StateNotifier<ScreenState> which is unwanted.

lukaspili avatar Nov 19 '20 22:11 lukaspili

No it won't.

You'd need to do ref.watch(detailsProvider.state)

rrousselGit avatar Nov 19 '20 23:11 rrousselGit

Mmh I can't do ref.watch(detailsProvider.state), only ref.watch(detailsProvider) or ref.watch(detailsProvider).state.

From my tests, this provider returns a new instance every time detailsProvider gets a new value:

final _controllerProvider = StateNotifierProvider.autoDispose<_Controller>((ref) {
  ref.watch(detailsProvider);
  return _Controller(ref.read);
});

lukaspili avatar Nov 20 '20 03:11 lukaspili

Oh I thought you were using a StateNotifierProvider

Then create a separate provider to capture only what you need:

final detailsProvider = StateProvider(...);

final detailsControllerProvider = Provider<StateController>((ref) => ref.watch(detailsProvider));

final _controllerProvider = StateNotifierProvider.autoDispose<_Controller>((ref) {
  ref.watch(detailsControllerProvider);
  return _Controller(ref.read);
});

This way the provider won't rebuild when the state changes

rrousselGit avatar Nov 20 '20 04:11 rrousselGit

Ok makes sense - thanks! :) Regarding your first statement "But I do consider disallowing context.read on .autoDispose providers to prevent any mistake from happening", does that apply to Reader.read too?

lukaspili avatar Nov 20 '20 05:11 lukaspili

does that apply to Reader.read too?

Yes. The problem is that it makes the function very inconvenient to use.

rrousselGit avatar Nov 20 '20 05:11 rrousselGit

I agree. I'm curious how you would personally design this kind of provider relations then, but I guess it's off topic. In my opinion, the doc would benefit from a series of cookbook/recipes illustrating the best practices on various scenarios like the ones discussed here.

lukaspili avatar Nov 20 '20 15:11 lukaspili

watch(myProviderThatShouldStayAlive);

@rrousselGit should read works in the same way or we need to use watch to keep it alive? If not, why?

David-Mou avatar Jun 02 '21 08:06 David-Mou

@David-Mou only watch. Read should not even be used in build method.

Watch makes the widget to observe the provider, creates dependency and makes the provider not to dispose. Read on the other hand, is only for one time read of a value from a provider.

KristianBalaj avatar Jun 02 '21 14:06 KristianBalaj

@David-Mou only watch. Read should not even be used in build method.

Watch makes the widget to observe the provider, creates dependency and makes the provider not to dispose. Read on the other hand, is only for one time read of a value from a provider.

He mean, if read() still alive same as watch() @David-Mou yes it works in the same way. Just difference as @KristianBalaj say.

iamarnas avatar Jun 02 '21 14:06 iamarnas

Covered by https://docs-v2.riverpod.dev/docs/essentials/eager_initialization

rrousselGit avatar Oct 10 '23 17:10 rrousselGit