Coder Social home page Coder Social logo

Comments (37)

rrousselGit avatar rrousselGit commented on August 21, 2024 2

Don't worry too much about the implementation. It's definitely doable to do the recursion only the first time
Once a provider's state was instantiated, all future reads hit a cache.

The issue is the first read. We still want it to be as fast as possible.

But I agree with you. This feature is likely needed. I just need some time thinking about how it could be in its ideal state.

from riverpod.

rrousselGit avatar rrousselGit commented on August 21, 2024

Thanks for the issue!
This is something I've been thinking of a few times.

I'm not exactly sure about it, because it makes the provider resolution slower as we need to do a recursion to check that none of its dependencies are overridden

from riverpod.

rrousselGit avatar rrousselGit commented on August 21, 2024

It's worth noting that if you override your provider at the root of your app, this applies the change for everything.

Would this work in your case?
Or do you actually override a provider for a specific widget tree?

If so, why do you need to do that override?

from riverpod.

TimWhiting avatar TimWhiting commented on August 21, 2024

I just updated the diagram and the code example for reference to address your questions.

As far as your first comment about the provider resolution; could the results of the resolution be cached in some way so that you don't have to do the recursion every time? Such as just collapsing the recursion into a list, and make sure those providers haven't been overridden since the last build.
Or even at the overrideForSubtree call site, automatically also override the providers that register themselves as unique dependencies:
Don't know if this would actually work since I don't know how you implemented this.

final idProvider = Provider((_) => 1)
final storageProvider = Provider((ref) => StorageProvider(ref), uniqueForDependencies: [idProvider])

// Widget tree
idProvider.overrideForSubtree(Provider((_) => 2))
// This call also overrides storageProvider since it registered itself as a dependency
// that should be unique per idProvider

As far as your second comment. I'm not sure if I understand what you are trying to say.
I need a root ProviderScope for providing some common things for both (the server config & service). I need the rest of my providers to be overridden, but since they all depend on one provider (unique id). I want some way to specify that they need to be unique depending on the unique id provider in their scope.
So, I'm pretty sure the answer to your question is that I actually do override a provider for a specific widget tree. (but in my case the widget tree is a material app).

from riverpod.

rrousselGit avatar rrousselGit commented on August 21, 2024

I wonder if maybe we instead want a ComputedFuture / ComputedStateNotifier / ...

final idProvider = Provider((_) => 1)
final storageProvider = Computed((read) {
  return StorageProvider(read(idProvider));
});

and track what is used in the body of the Computed.

from riverpod.

TimWhiting avatar TimWhiting commented on August 21, 2024

Exactly. I think that is what this is requesting. A way to have a StateNotifierProvider or FutureProvider that tracks what is being used like a Computed. Is this possible?

from riverpod.

rrousselGit avatar rrousselGit commented on August 21, 2024

We can do it for cases where ref.read is called within the body of the provider:

final a= Provider(...);
final b = Provider((ref) {
  return Something(ref.read(a));
});

On the other hand, if ref.read is called asynchronously after the object creation (like when ref is passed to the object), that won't be feasible.

from riverpod.

TimWhiting avatar TimWhiting commented on August 21, 2024

Ah, that is just what I was going to ask. What if the ones that you want to track are called synchronously, but providers that you don't care about tracking being called asynchronously?

from riverpod.

TimWhiting avatar TimWhiting commented on August 21, 2024

Essentially I would like a read and a ref.

final a= Provider(...);
final b = Provider((ref, read) {
  return Something(ref, read(a));
});

class Something {
  int id;
  ProviderReference ref;
  Something(this.ref, this.id);
}

from riverpod.

rrousselGit avatar rrousselGit commented on August 21, 2024

That's feasible but sounds oddly confusing.

Having ref.read behave differently based on where it's called sounds like a bad idea.
And having read vs ref.read sound confusing too.

from riverpod.

rrousselGit avatar rrousselGit commented on August 21, 2024

Maybe we could go with:

final a= Provider(...);
final b = Provider.scoped((ref) {
  // ref.read can only be called within the body of the provider
  // otherwise it is an error
  return Something(ref.read(a));
});

But still, that sounds confusing.

from riverpod.

TimWhiting avatar TimWhiting commented on August 21, 2024

Hmm

final a= Provider(...);
final b = ComputedProvider((ref, computedReader) {
  return Something(ref, computedReader(a));
});

/// Or

final b = Provider.scoped((ref, key) {
  return Something(ref, key(a));
});

/// Or

final b = Provider.familyFactory((ref, key) {
  return Something(ref, key(a));
});

from riverpod.

rrousselGit avatar rrousselGit commented on August 21, 2024

Another path is to go for:

final idProvider = Provider((_) => 1)
final storageProvider = Provider((ref) => StorageProvider(ref), uniqueForDependencies: [idProvider])

but have a linter warn for missing dependencies inside uniqueForDependencies.

from riverpod.

TimWhiting avatar TimWhiting commented on August 21, 2024

I honestly like that idea better. I'd prefer to mention what I want to be unique on explicitly like that.
Because there are some providers that I want to be unique based off of (storageid), and some that I just want access to (services).

from riverpod.

TimWhiting avatar TimWhiting commented on August 21, 2024

Another pattern that this opens up is something like the following:

final allContacts = [Contact(), ....];
final indexProvider = StateProvider((_) => 1)
final contactProvider = ProviderFamily<Contact, int>((_, index) => allContacts[index]);
final currentContactProvider = Provider((ref) => contactProvider(ref.read(indexProvider)), uniqueForDependencies: [indexProvider])

A way to combine the family and unique for dependencies would make this pattern even better.

I know you can do this using a ProviderFamily by passing the index around your widget tree. But that seems kind of clunky sometimes to me. Especially if you want to change the index from lower in the tree.

from riverpod.

rrousselGit avatar rrousselGit commented on August 21, 2024

Another though is to have:

@scoped
final idProvider = Provider((_) => 1)

final storageProvider = Provider((ref) {
  ref.read(idProvider); // warns, avoid ref.read on @scoped providers

  // equivalent to ref.read, but can be called only within. the provider's body
  // If the dependency listened is overridden, this provider will be overridden too
  ref.track(idProvider);
});

from riverpod.

rrousselGit avatar rrousselGit commented on August 21, 2024

A way to combine the family and unique for dependencies would make this pattern even better.

I know you can do this using a ProviderFamily by passing the index around your widget tree. But that seems kind of clunky sometimes to me. Especially if you want to change the index from lower in the tree.

Computed sounds more logical:

final allContacts = [Contact(), ....];
final indexProvider = StateProvider((_) => 1)
final contactProvider = ProviderFamily<Contact, int>((_, index) => allContacts[index]);
final currentContactProvider = Computed((read) => read(contactProvider(read(indexProvider))));

from riverpod.

TimWhiting avatar TimWhiting commented on August 21, 2024

Oh, I like adding ref.track and the lint annotation.

from riverpod.

TimWhiting avatar TimWhiting commented on August 21, 2024

Computed sounds more logical:

final allContacts = [Contact(), ....];
final indexProvider = StateProvider((_) => 1)
final contactProvider = ProviderFamily<Contact, int>((_, index) => allContacts[index]);
final currentContactProvider = Computed((read) => read(contactProvider(read(indexProvider))));

Yes, but what if the thing you were computing was not just a plain dart object, but rather a provider of some sort. i.e. StateNotifier, does that approach work in that case?
i.e.

final allContacts = [Contact(), ....];
final indexProvider = StateProvider((_) => 1)
final contactProvider = ProviderFamily<Contact, int>((_, index) => allContacts[index]);
final currentContactProvider = Computed((read) => ContactEditorProvider(read(contactProvider(read(indexProvider)))));

from riverpod.

TimWhiting avatar TimWhiting commented on August 21, 2024

Okay, that's how you would do that. Thanks, I'm sure that will come in handy sometime!

from riverpod.

TimWhiting avatar TimWhiting commented on August 21, 2024

Question, does the family providers dispose of their cache at any point if the providers are not used anymore (Like an autodispose provider family)? From looking at the source code it didn't look like it. What if the user has scrolled through 100s of contacts or whatever is in a family?

from riverpod.

rrousselGit avatar rrousselGit commented on August 21, 2024

The parameters passed to families are never destroyed.
But the state associated to a parameter may get destroyed, when combined with .autoDispose

Provider.autoDispose.family<Foo, int>((ref, id) {...});

from riverpod.

rrousselGit avatar rrousselGit commented on August 21, 2024

This will likely change in the future. I'm thinking about how to fix that.

It's not critical though

from riverpod.

rrousselGit avatar rrousselGit commented on August 21, 2024

So...
Imo the true solution is using families:

final idProvider = Provider((ref) => 1);
final storageFamily = Provider.family<Storage, int>((ref) => Storage(id));


final currentId = useProvider(idProvider);
useProvider(storageProvider(currentId));

This doesn't require using overrides, which I think makes things easier to understand.
The problem is the verbosity of consuming the provider.

Maybe we can have some utilities to "bind" a family to a provider – like:

final userProvider = Provider((ref) => User(id: 42));

final _storageFamily = Provider.family<Storage, int>((ref) => Storage(id));
final storageProvider = _storageFamily.bind((read) => read(userProvider).id);

....

useProvider(storageProvider);
// Equivalent of:
useProvider(storageProvider(useProvider(userProvider).id));

This solves the UI reading the family without the UI.
The remaining question is how can another provider read a .family(...).bind

from riverpod.

rrousselGit avatar rrousselGit commented on August 21, 2024

Other thought on the .scoped, by splitting the creation into two callbacks instead of one:

final userProvider = Provider((ref) => 1);

final storageProvider = Provider.scoped<Storage, int>(
  (ref, id) => Storage(id),
  from: (read) => read(userProvider).id,
);

final otherProvider = Provider.scoped<Other, Storage>(
  (ref, storage) => Other(storage),
  from: (read) => read(storageProvider),
);

The from: function would be pure function, and whenever one of the dependencies used inside from: is overridden, the associated provider is overridden too.

from riverpod.

rrousselGit avatar rrousselGit commented on August 21, 2024

A more flexible implementation of the previous .scoped may be:

final userProvider = Provider((ref) => 1);

final storageProvider = Provider.scoped<Storage>((read) {
  final user = read(userProvider);

  return (ref) => Storage(user.id);
});

final otherProvider = Provider.scoped<Other>((read) {
  final user = read(userProvider);
  final storage = read(storageProvider);

  return (ref) => Other(storage, user);
});

Or alternatively:

final userProvider = Provider((ref) => 1);

final storageProvider = Provider.scoped<Storage, User>(userProvider, (ref, user) {
  return Storage(user.id);
});

final otherProvider = Provider.scoped2<Other, Storage, User>(storageProvider, userProvider, (ref, storage, user) {
  return Other(storage, user);
});

I like both of these approaches.

The former is more flexible but scary looking and can be misused.
The latter is easier to use but a bit verbose and requires scoped vs scoped2 vs scopedX.

In any case, this is similar to the uniqueDependencies proposal – but safer as doesn't need a linter.

from riverpod.

TimWhiting avatar TimWhiting commented on August 21, 2024

I agree it can be solved using families. However, the issue is that I would have to have lines like this in almost every widget.

final currentId = useProvider(idProvider);
useProvider(storageProvider(currentId));

Or pass down the id through every widget constructor. And I would have families depending on families and just passing the id all the way up the family chain.

The bind syntax would make the above more reasonable, but still I would prefer one line to define my provider rather than creating a family + a bind for each provider I have. The issue with using computed for the binding portion is then you can't depend on the computed in a provider. (As far as I've tried at least). So if the bind would cause a similar issue of not being able to depend on it then it still wouldn't work.

As far as your last proposal, I prefer the first alternative, unless the types can be inferred. Requiring scoped ... scopedX is also a downside of the second proposal.

I like your from proposal as well, however it's not clear how it would scale to multiple dependencies.

from riverpod.

rrousselGit avatar rrousselGit commented on August 21, 2024

By the way @TimWhiting, do you really need the override feature for this?

I'm thinking that this problem could be heavily simplified without the ability to locally override a provider

from riverpod.

TimWhiting avatar TimWhiting commented on August 21, 2024

I'm not tied to the idea of overriding, what did you have in mind? @rrousselGit

from riverpod.

rrousselGit avatar rrousselGit commented on August 21, 2024

The only reason we do not have a ComputedFuture/..., or a Provider.scoped (with the "ref.track") is because we can override a provider for part of the subtree.

It makes things bloody difficult to know where in the graph of ProviderScope should a Computed-like be inserted.

from riverpod.

TimWhiting avatar TimWhiting commented on August 21, 2024

@rrousselGit
Makes sense. Would there be a good equivalent for testing if you remove overriding, and add in the ComputedFuture / provider.scoped? I guess there could be a flag for test mode in the top of the tree, and the computed providers could choose whether to insert a mock or a real service depending on the flag. The issue with that is that it brings your testing logic (mocks) into your actual application.

An alternative would be to let people override still for testing, but not at any level or any subtree. That way you only have to replace providers 1 for 1, and not worry about individual subtrees.

from riverpod.

rrousselGit avatar rrousselGit commented on August 21, 2024

Overrides would still be there, but only global to the entire app

from riverpod.

TimWhiting avatar TimWhiting commented on August 21, 2024

Thinking some more, how would you accomplish what I am looking for without overrides for a specific tree? I have essentially 2 MyApp instances side by side in a row. They have nothing different passed into their constructor and they use the same providers. How does one resolve to one set of providers (computed or scoped) and the other resolve to a different set?

from riverpod.

rrousselGit avatar rrousselGit commented on August 21, 2024

I'm currently making a separate issue to explain my entire plan

The basic idea is:

  • all existing providers can only be overridden globally
  • add ComputedChangeNotifier/ComputedFuture/...
  • add a third kind of value, that can be locally overridden locally in a widget tree, but is just a simple value with no extra fancy thing.

The last one would be:

final appId = ScopedProvider<int>();


routes: {
  '/': (_) => ProviderScope(overrides: [appId.overrideAs(42)], child: App()),
  '/settings': (_) => ProviderScope(overrides: [appId.overrideAs(21)], child: App()),
}

Where ScopedProvider cannot be obtained using ref.read.

Instead, it would have to be reconciled by the widget tree, so:

final appIdProvider = ScopedProvider<int>();
final appProvider = StateNotifierProvider.family<App, int>((ref, id) {});

...
final appId = useProvider(appIdProvider);
final app = useProvider(appProvider(appId));

from riverpod.

TimWhiting avatar TimWhiting commented on August 21, 2024

Okay, yeah, a separate local provider that has to be either provided or resolved in the tree seems like the best solution. In fact while thinking about how to deal with no overriding, I thought that I could always use the old provider package to only provide something to a specific portion of the tree. Seems like that ability to explicitly say where to provide what is still useful, though I really like how riverpod gets most of the providers out of being explicitly stated in the ui tree.

I just shared a repository with a simple counter example app that mimics what I'm doing right now with my games app, but on a smaller scale. Feel free to mess around with it when you are trying this out.

from riverpod.

rrousselGit avatar rrousselGit commented on August 21, 2024

Agreed.
But it's probably better to implement a ScopedProvider used like other providers in riverpod, rather than using package:provider

The fact that we can't provide two values of the same type is very limiting in this situation, as we only want to expose primitives.
We could have a clash if we have two values like appId exposed. And it overall fits with the package anyway

from riverpod.

TimWhiting avatar TimWhiting commented on August 21, 2024

Agreed

from riverpod.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.