riverpod
riverpod copied to clipboard
Disposing provider's state
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?
Have you tried using maintainState = true;
inside the productsStateProvider
@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.
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 confirming this solution with const
.
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 itemWidget B
(so B is built after A) - On init,
Widget A
triggers a chain of reaction which ends pushing a value toStateProvider B
-
Widget B
watches onStateProvider B
-
StateProvider B
isautoDispose
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.
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
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.
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 .
What do you mean?
Oh sorry the message wasn't sent properly
I meant instead of:
ref.dependsOn(detailsProvider)
do:
ref.watch(detailsProvider)
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.
No it won't.
You'd need to do ref.watch(detailsProvider.state)
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);
});
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
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?
does that apply to Reader.read too?
Yes. The problem is that it makes the function very inconvenient to use.
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.
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 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.
@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.
Covered by https://docs-v2.riverpod.dev/docs/essentials/eager_initialization