rrousselGit / riverpod

A reactive caching and data-binding framework. Riverpod makes working with asynchronous code a breeze.
https://riverpod.dev
MIT License
6.34k stars 960 forks source link

Auto dispose after first listener #1329

Closed talent-apps closed 2 years ago

talent-apps commented 2 years ago

Riverpod version 2.0.0-dev.4 adds a disposeDelay feature to all autoDispose providers and to ProviderContainer/ProviderScope. This configures the amount of time before a provider is disposed when it is not listened.

To avoid timing issues, it would be nice to add a "auto dispose after the provider is listened to once" mode, which make the provider auto disposable only after it is listened to once.

Example:

XuanTung95 commented 2 years ago

Well ultimately you can do that yourself already

Provider.autoDispose((ref) { ref.keepAlive(); ref.onCancel(ref.invalidateSelf); } This effectively produces the same result as autoDispose, without the state getting destroyed during navigation

Check out https://github.com/rrousselGit/river_pod/issues/1329#issuecomment-1086828636 That looks like a bug with StateProvider (& likely State/ChangeNotifierProvider)

@rrousselGit

I'm confused.

With this code, I expected ref.onCancel(ref.invalidateSelf) will cause invalidates the state of the provider before go to screen 2 because ref.invalidateSelf is called. If the expected behavior is keeping the state during navigation then no one will understand why, it's more like a bug to me if you implement that.

rrousselGit commented 2 years ago

With this code, I expected ref.onCancel(ref.invalidateSelf) will cause invalidates the state of the provider before go to screen 2 because ref.invalidateSelf is called.

No it won't.

ref.read does not triggers onCancel

onCancel triggers when the last listener is removed. ref.read doesn't add/remove listeners, so it won' trigger onCancel,

The bug with StateProvider is because it's implemented as two providers (StateProvider vs StateProvider.notifier, where the former "watch" the later). So with autoDispose, a listener is added then removed immediately

rrousselGit commented 2 years ago

But I think I'll make a breaking change and:

So doing:

Button(
  onTap: () => ref.read(autoDisposeProvider.notifier).state++,
)

and then tapping the button will prevent the provider from getting disposed until the button is unmounted.

That's effectively what we've discussed before and what I originally wanted to do for the 1.0.0. This change is part of why context.read was changed to ref.read inside widgets – I just didn't go through with it.

Although I guess this won't cause the provider to be disposed when leaving screen2 and we'd have to leave screen 1. 🤔

TimWhiting commented 2 years ago

I like this idea of the breaking change. I have definitely run into problems related to those apis.

XuanTung95 commented 2 years ago

Although I guess this won't cause the provider to be disposed when leaving screen2 and we'd have to leave screen 1. 🤔

@rrousselGit Let's consider it a bit.

I think people still want to read a provider everywhere without the need to keep it alive. If everything is "watch" then it's hard to auto dispose a provider because we need to care about every read in the whole app.

The scenario of this ticket can be solved easily by using ref.watch on screen 1, it is just folks don't want rebuild when it's not necessary.

I think we should work on how to "read" an autoDispose provider and keep it alive until it's done.

talent-apps commented 2 years ago

@XuanTung95 I agree that making the read API into a watch API is counter intuitive. The scenatio that was initially presented was that screen 1 doesn't care about the provider, and therefore doesn't need to watch it, it only initialized it with some value for screen 2.

XuanTung95 commented 2 years ago

If I have a normal provider or family which is not .autoDispose. Later I want to .autoDispose it, could I have a method dontKeepAlive? The workaround would be to create .autoDispose and set keepAlive then store the KeepAliveLink, then close() it, which is a lot of code.

I don't understand what you're trying to do. Could you share some code?

@rrousselGit

I edit my example. It looks like this.

final myProvider = ChangeNotifierProvider((ref) {
  Future.delayed(Duration(seconds: 2)).then((value) {
    ref.autoDispose(); // <- this provider become autoDispose from now on
  });
  Future.delayed(Duration(seconds: 5)).then((value) {
    ref.keepAlive(); // <- back to normal
  });
  return ChangeNotifier();
});

Goal: Add 2 method .autoDispose() and .keepAlive() to Ref<State extends Object?>

I think this feature will complete the .keepAlive() implementation you did. Allow folks to controller the life-cycle of provider more flexible. It can solve the issue of this ticket by using .autoDispose() method once screen B is disposed.

rrousselGit commented 2 years ago

That's already doable:

final myProvider = ChangeNotifierProvider.autoDispose((ref) {
  var link = ref.keepAlive();
  Timer(Duration(seconds: 2), () {
    link.cancel();
  });
  Timer(Duration(seconds: 5), () {
    ref.keepAlive();
  });
  return ChangeNotifier();
});
XuanTung95 commented 2 years ago

I know, but it's only possible for .autoDispose, other providers also need to be disposed if nessesarry. The current implementation is not friendly if I don't want to use .autoDispose.

rrousselGit commented 2 years ago

That's not a bug, that's a feature. Marking a provider as .autoDispose is critical for making hard-to-catch bugs a compilation error instead. It being necessary is voluntary and this will not be changed.

XuanTung95 commented 2 years ago

.autoDispose is critical for making hard-to-catch bugs a compilation error instead

Could you give an example bug? Because most bugs I got is from auto thing.

rrousselGit commented 2 years ago

Listening an autoDispose provider in a not autoDispose provider:

final provider = Provider((ref) {
  ref.watch(autoDisposeProvider);
});

This will cause the autoDispose provider to never be disposed once provider is initialized, which is almost always undesired and very difficult to debug.

This is currently voluntarily made to be a compilation error, and is the reason why we do Provider.autoDispose(...) instead of Provider(..., autoDispose: true)

XuanTung95 commented 2 years ago

This is currently voluntarily made to be a compilation error

I understand. But it has nothing to do with my proposal. The normal provider still cannot watch an autoDispose provider. It just add a away to dispose the normal provider.

rrousselGit commented 2 years ago

Your proposal (ref.autoDispose()) introduces the same problem:

final a = Provider((ref) {
  ref.autoDispose();
});

final b = Provider((ref) {
  ref.watch(a); // cause a to never be disposed
});

This should be a compilation error, but isn't with your proposal.

Using .autoDispose here is desired. By doing what I suggested here https://github.com/rrousselGit/river_pod/issues/1329#issuecomment-1088794571 then you would indeed get a compilation error if you tried to watch the provider in a non-autoDispose provider.

XuanTung95 commented 2 years ago

They are 2 different use cases. If I call ref.autoDispose(), I should expect it do not dispose if someone is listening. So how can it be a bug if I want it to behave that way.

final a = Provider((ref) {
  ref.autoDispose();
});

final b = Provider((ref) {
  ref.watch(a); // cause a to never be disposed => it's not true now because b can be disposed by using .autoDispose()
});
rrousselGit commented 2 years ago

On a different note, what do you think about a simplification of ref.listen + close

onTap: () {
  await ref.run(provider, (value) async {
    // provider will stay alive until the callback completes
    await Navigator.push(); // The await here will keep the provider alive until the route is poped
  });
}
robpot95 commented 2 years ago

On a different note, what do you think about a simplification of ref.listen + close

onTap: () {
  await ref.run(provider, (value) async {
    // provider will stay alive until the callback completes
    await Navigator.push(); // The await here will keep the provider alive until the route is poped
  });
}

This make more sense, LGTM

XuanTung95 commented 2 years ago

On a different note, what do you think about a simplification of ref.listen + close

onTap: () {
  await ref.run(provider, (value) async {
    // provider will stay alive until the callback completes
    await Navigator.push(); // The await here will keep the provider alive until the route is poped
  });
}

I think we can do the same with this.

onTap: () async {
  final value = ref.read(provider);
  final link = value.keepAlive();
  await Navigator.push();
  link.close();
}

Adding run() method may be not necessary unless it is used a lot. And "go_router" package does not return Future when navigating, so it only work for Navigation 1.0.

rrousselGit commented 2 years ago

You cannot call keepAlive like you've described.
And you also need to handle exceptions, as if somehow code after the read throws, the provider will never get disposed.

And "go_router" package does not return Future when navigating, so it only work for Navigation 1.0.

That's not Riverpod's fault though. You can make a feature request for it in go_router's repository

XuanTung95 commented 2 years ago

It has been a while, any update on this ticket?

rrousselGit commented 2 years ago

I'm still skeptical about this. I think this would be a hazardous thing to implement and could cause quite a bit of confusion I'd prefer looking at a better solution. As once published, the harm done by such a feature could be enormous.

For this issue, I think a better alternative would be to give you the primitives to be able to implement this on your own. I think the ref.onCancel/ref.onResume life-cycles are good candidates (combined with ref.keepAlive())

You should be able to use those to implement this on your own, without needing Riverpod to make this an official API.

rrousselGit commented 2 years ago

Closing in favor of https://github.com/rrousselGit/riverpod/issues/1665. That issue would add the missing feature for implementing what's described here.

AhmedLSayed9 commented 1 month ago

@rrousselGit Should we reopen this?

It was closed in favor of ref.onCancel but ref.onCancel is not reliable to be used for this use-case as described at #2992 & #3759.

rrousselGit commented 1 month ago

Nope. I have no plan to change this.

IMO this feature is too dangerous, as if the "first listener" is never added for some reason, the disposal will never happen.

We can raise new issues with regads to documenting how to init some values before pushing a route though.