-
-
Notifications
You must be signed in to change notification settings - Fork 961
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[RFC] Unifying syntax for listening to providers (v2) #335
Comments
Easier to read |
I like most of it but I am not sure that I like the approach of a new When working with hooks we have a "pre-understanding" that we can use the I don't really see how the benefits outweighing the negatives in this one. To me this would be a lot more verbose because we would have to refactor a |
Following what @RobertBrunhage just said. If I'm not mistaken, the hook change would also mean that custom hooks that read from a provider now would need to receive a |
I like the new API ! I'm not familiar with riverpod_hooks, but i'm a bit confused that there are two separate packages / API's built on riverpod. Can someone explain why we need both riverpod_flutter and riverpod_hooks? Also why have a ConsumerWidget / ConsumerHookWidget when you could just use a Consumer? I think removing the first two widgets would simplify the API and make it easier to get into. |
The ConsumerHookWidget is mainly for maintainability, to avoid having to duplicate work. The issue with the current approach is, if I add a |
I honestly like the proposal, only thing I don't really like is the necessity to use |
No, a mixin on StatelessWidget is not feasible. |
@gaetschwartz do you have an example of what would he tedious to update? Depending on the code, it may be doable to implement a migration tool. |
This is quite a big breaking change imo. Then again Riverpod is still a 0.x.x version which basically includes that anything still goes from API dev point of view as well. Still for those already using Riverpod a lot of code will need to be changed, trivial refactors, but still quite a bit. I actually don't like the new syntax that much, it is more verbose and not as pretty as before. Applies to the watch and especially hooks. Read is basically equivalent, so no real diff, but a refactor as well. I like what this enables and appreciate that part, but it is not as pretty as before and anybody that has been using Riverpod will have quite a bit of work ahead of them to migrate to this new syntax. On the plus side (in addition to the listeners) it does make things more consistent at the same time, which probably is less confusing for newcomers, so that is probably a good thing. |
Just saying that imagine having the most simple StatelessWidget: class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return TextButton(
onPressed: () {
context.read(myProvider).doStuff();
},
child: const Text('Tap me!'));
}
} You have to:
These changes can be trivial to advanced developers by using replace string/regex in VSCode etc but it is probably going to be tedious for beginners/newcomers. Providing instructions to semi-automatically migrate using regexp and search & replace on VSCode would be a good idea. Tldr; if you are to make these changes it would be great to either give proper tools to migrate or make it easier to do so because as it is it's too much of a breaking change and would make it a long migration for middle/large sized projects. Where most users wouldn't actually see an improvement to their usage of riverpod so the cost/benefit wouldn't be optimal. |
Have nothing against the maintainability part but I am afraid that in my point of view this will only make Hooks more complicated to get in to for newcomers. Regarding what others have said about refactoring I agree that it will be a bit of a pain point but we also have to be aware that this is stated in the Readme file The API may change slightly when more features are added, and is something we should come to expect. Personally I don't want to limit the refactoring part of making Riverpod easier to use before it hits 1.0.0 as that is when it will be harder to change these core implementations. |
About readability, it's worth noting that as Dart evolves, the syntax will likely get better naturally For example, object destructuring could be a candidate: class StatelessExample extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetReference(read)) {
return ElevatedButton(
onPressed: () => read(counterProvider).state++;
);
}
} |
I agree with @RobertBrunhage on the last days I've been sharing content about Riverpod as a great tool, but people get afraid to fast. I think this change will only be a new tiny thing on the learning curve that riverpod brings with it. |
The only concern I have, since there is no real difference between |
I like the proposal. Even for large projects it shouldn‘t be too much effort to migrate by updating Riverpod and just quickly fixing the Analyzer issues. I share the thoughts of @RobertBrunhage on ConsumerHookWidget but I can accept that for the sake of consistency and having a simpler API for beginners who probably start using Riverpod without hooks. |
On the plus side for hooks, Column(
children: [
if (something)
Text(ref.watch(provider)),
Text('Hello world'),
],
) This is not something doable with |
I don’t understand something. If the syntax using hooks becomes the same as it would if we didn’t use hooks, what would be the added value of hooks? |
That you get to use hooks So you can do: class HooksExample extends ConsumerHookWidget {
@override
Widget build(BuildContext context, WidgetReference ref) {
A value = ref.watch(a);
final controller = useTextEditingController();
}
} |
As a user of if the reason for it all is to have a
and have a plain HookWidget this would also allow for conditionals and loops |
I am in agreement with everyone. I share some concerns over deprecating // In a custom hook
X useProviderXListenAndShowSnackbar() {
final ref = useWidgetsReference();
final context = useContext();
final X = ref.listen(providerOfX, () {
if (X.someCondition) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar());
}
});
return X;
}
// Inside ConsumerHookWidget
Widget build(BuildContext context, WidgetReference ref) {
final state = ref.watch(provider);
} The main other complain I'm hearing is the tediousness of migration. I'm willing to help create a migration tool. I'll open another issue to talk about a migration tool. |
@TimWhiting Just a clarification, I'm most probably wrong, as I often am, but by returning the reference through a hook, you should be free to call |
@dancamdev I'm just saying that adding an extra parameter in the build method takes up fewer lines of code and makes the api more consistent allowing for easier refactorings between But I agree that we need a hook for getting a |
final myProvider = StateProvider<bool>((ProviderContext context) {
final another = context.read(anotherProvider);
final oneMoreProvider = context.watch(oneMoreProvider);
});
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final another = context.read(anotherProvider);
return Text(anotherProvider);
}
}
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Consumer(builder: (context, watch, _) {
final oneMoreProvider = context.watch(oneMoreProvider);
return Text(oneMoreProvider);
})
;
}
}
class MyWidget extends ConsumerWidget {
@override
Widget build(BuildContext context) {
final oneMoreProvider = context.watch(oneMoreProvider);
return Text(oneMoreProvider);
}
}
class MyWidget extends ConsumerHookWidget {
@override
Widget build(BuildContext context) {
final oneMoreProvider = context.watch(oneMoreProvider);
return Text(oneMoreProvider);
}
}
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final oneMoreProvider = context.watch(oneMoreProvider);
return Text(oneMoreProvider);
} Everything is pretty much self explanatory. This way you can have context.container, context.refresh and etc. |
|
I can't seem to find Currently when using ConsumerStatefulWidget, I get error saying that I can only call hooks from widgets that mix-in hooks. |
Same here.. Is there any other way accesing provider in initState ? |
ConsumerStateMixin became ConsumerStatefulWidget + ConsumerState It was not feasible to implement it with just a mixin |
@rrousselGit I see. I did that, but when using
Here's a snippet of the stateful widget: class UploadPage extends ConsumerStatefulWidget {
final File? image;
UploadPage(this.image);
@override
_UploadPageState createState() => _UploadPageState();
}
class _UploadPageState extends ConsumerState<UploadPage> {
bool uploading = false;
@override
Widget build(BuildContext context) {
final nameController = useTextEditingController();
.....
} |
If you want to use hooks, use hooks_riverpod with HookConsumerWidget |
While I like many of these changes, I do have a few points I would like to address:
Thank you for reading! |
That shouldn't lock you into using Riverpod forever. If it takes 2 seconds to switch from StatelessWidget to ConsumerWidget, it also takes 2 seconds to do it the other way around.
There's no issue with this.
As mentioned previously, this requires too much work. I personally don't see the issue with requiring hooks that want to read providers to receive a "WidgetRef" as parameter: useSomething(WidgetRef ref) {
useWhatever()
ref.watch(provider);
} The difference in verbosity is minimal. |
This proposal makes a lot of sense for everything other than hooks consideration. Adding As for the unifying syntax fact, I want to use hooks_riverpod BECAUSE I want to use hooks WITH riverpod. I WANT to use It should be simple (which is the case for non hooks consideration) and intuitive for the given context. |
I think it's hard to migrate hooks in automated way. Well it may be a special case of me, but I can't see contagious parameter insertion is trivial.
I don't think you didn't consider this, therefore I'm curious why there's disagreement in this issue.
The above will be
It's similar to prop drilling. I think it is one of major reason of using DI library. If it's fine then why not use plain Map? |
It does seem a shame for idiomatic Flutter development that it's not
possible to obtain a provider's value using an of(context) method.
final Counter = Provider<int>(...);
final counter = Counter.of(context);
And if such we're possible then presumably there'd be no need for passing
the WidgetRef to hooks.
Appreciate that may be much easier said than done.
…On Thu, 16 Sep 2021, 07:50 jeiea, ***@***.***> wrote:
That shouldn't lock you into using Riverpod forever. If it takes 2 seconds
to switch from StatelessWidget to ConsumerWidget, it also takes 2 seconds
to do it the other way around.
I think it's hard
<#335 (comment)>
to migrate hooks in automated way. Well it may be a special case of me, but
I can't see contagious parameter insertion is trivial.
The difference in verbosity is minimal.
I don't think you didn't consider this, therefore I'm curious why there's
disagreement in this issue.
useA() { useB(); }
useB() { useC(); }
useC() { useD(); }
useD() { useE(); }
useE() { useRepository(); }
The above will be
useA(ref) { useB(ref); }
useB(ref) { useC(ref); }
useC(ref) { useD(ref); }
useD(ref) { useE(ref); }
useE(ref) { useRepository(ref); }
It's similar to prop drilling. I think it is one of major reason of using
DI library. If it's fine then why not use plain Map?
—
You are receiving this because you commented.
Reply to this email directly, view it on GitHub
<#335 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AAE6N6USYNEHBWS6GILEE7DUCGHU3ANCNFSM4X4F26PQ>
.
Triage notifications on the go with GitHub Mobile for iOS
<https://apps.apple.com/app/apple-store/id1477376905?ct=notification-email&mt=8&pt=524675>
or Android
<https://play.google.com/store/apps/details?id=com.github.android&referrer=utm_campaign%3Dnotification-email%26utm_medium%3Demail%26utm_source%3Dgithub>.
|
Would |
I wonder how many feel that I only have to understand In many cases, we've got rid of hooks as they don't add much value vs. just calling Aside from having to pass ref around in some cases, are there any other objections to the new syntax? |
Note that one thing I'm hopeful for is, once we have metaprogramming, we may be able to straight up remove final labelProvider = Provider((ref) => 'Hello world');
class Example extends StatelessWidget {
@override
Widget build(context) {
final message = @watch(labelProvider);
return Text(message);
}
} And it likely would work within hooks too. Still, we'll see if the Dart team will really allow such macros. |
I just read that and orgasmed. We want static metaprogramming so bad. |
I do not like it that much - it surely makes the code less readable / more difficult to understand / to dive into as you cannot easily view the source. It really is not any effort to write out |
To be clear, using metaprogramming here isn't about reducing a bit the numbers of characters we have to type. The primary use-cases are:
In particular, a common problem with the v1, where we can now scope all providers, is that folks tend to override a provider but forget to override its dependencies. Such that we have: final provider = Provider((ref) => 'Hello')
final another = Provider((ref) => ref.watch(provider))
ProviderScope(
overrides: [
provider.overrideWithValue('Bonjour'),
],
child: Consumer(
builder: (context, ref, _) => Text(ref.watch(another)) // will print Hello, not Bonjour
)
) The fix is to do: ProviderScope(
overrides: [
provider.overrideWithValue('Bonjour'),
another, // tell Riverpod to scope the 'another' provider too
], But it seem confusing. So one solution I have in mind, providers could list their dependencies like so: final provider = Provider((ref) => 'Hello')
final another = Provider((ref) => ref.watch(provider), dependencies: {provider}); such that when we override Hence the idea of using metaprogramming here. Where folks would write: final provider = Provider((ref) => 'Hello')
final another = Provider((ref) => @watch(provider)); and that |
Please take people's criticism seriously, Remi. I agree with all the criticism mentioned so far. I would also like to put more emphasis on the argument of @scopendo, in that it would be great to use the context object for retrieval, the same way it works with the I'm aware you mentioned that this is not feasible, perhaps I just misunderstood the architecture, could you elaborate further? I'd imagine if I clearly see the advantages of |
One of the very reason Riverpod is a separate package from Provider is because
Using
In comparison, using |
@pikaju Additionally I'm interested in helping Remi with an analyzer plugin for riverpod that will likely include refactorings like the 'Wrap with widget' and 'Extract Widget' refactorings that are available from flutter, for example we could do:
Also I've discussed with Remi how to provide a migration tool from the old provider package to the riverpod package for legacy codebases. So if there is any change that you think would improve the experience of riverpod over provider without going back to the limitations of provider, I'm sure the suggestions would be welcome. |
@pikaju criticism is taken into consideration, but there are many different opinions on how riverpod should be designed, and the and some some difficult tradeoffs have to be considered. I personally love that riverpod is not coupled to BuildContext or Flutter at all. This makes testing pure Dart code much easier and would allow reuse of riverpod in other contexts such as AngularDart. Changing the base class is very fast. You have to to add/remove Consumer or HookConsumer and add/remove the WidgetRef parameter. It takes less than 5 seconds. The main criticism that might be valid is that, if a hook depends on In our codebase, hooks tend to be reserved for more general purpose functions for state management. Perhaps you could share some code samples of where you find your developer experience is worse with the new riverpod syntax? Without something specific, it will be hard to provide suggestions or find ways to improve riverpod. |
Thank you for the clarification @rrousselGit, I think I understand the problem in much more detail now. I also took a look at the My main goal was to apply the "composition over inheritance" rule to both Eventually I thought it would be better to just create a subtype of TLDR: You guys did the best you could, I surrender. All hail our lord and savior, metaprogramming. If at all possible, please try adding @TimWhiting I appreciate the effort, but I'm personally not a big fan of automatic code conversion tools. |
Believe me, I spent a lot of time trying to get And that includes making PRs in Flutter to change how InheritedWidgets works. I made a few experiments before Riverpod was a thing, with no success. Ultimately while it's a bit sad to have to rely on I wouldn't place too much hope in metaprogramming though. While I would love to be able to do |
Time to finally close this issue. Thanks for everyone's participation! |
This RFC is a follow-up to #246 with a slightly different proposal.
The problems are the same:
ProviderListener
that doesn't involve nesting, but not exclusively)ConsumerWidget
/Consumer
would be a large breaking changeConsumer
See #246 for a bit more explanation
Proposal
Instead of passing directly "watch" as parameter to widgets, Riverpod could do like with its providers and pass a "ref" object"
As such, instead of:
we'd have:
Similarly,
Consumer
would become:The behaviour would be strictly identical. But this then allows Riverpod to add extra methods on
WidgetsReference
, which could allow:This would be equivalent to
ProviderListener
but without involving nesting.Hooks consideration
While
hooks_riverpod
doesn't suffer from the problem listed at the start of the issue, the logic wants thathooks_riverpod
should also use the same syntax too (both to reduce confusion and simplify maintenance).As such,
useProvider
would be deprecated and aConsumerHookWidget
would be introduced. Which means that instead of:we'd have:
This would also clarify that the only purpose of
hooks_riverpod
is to use both hooks and Riverpod simultaneously.context.read/context.refresh considerations
context.read(myProvider)
andcontext.refresh(provider)
would be deprecated.Instead,
ref
should now be used. So the previous:would become:
(and same thing with
refresh
)This has two side-effects:
StatelessWidget
can no-longer be used to just "read" providerspackage:provider
and Riverpod forcontext.read
, which would simplify migration.StatefulWidget consideration
An optional goal of this change is to support
StatefulWidget
.This could be done by shipping a
State
mixin that adds aref
property, which would allow us to write:Note that this is entirely optional, as
Consumer
can already be used.ProviderReference vs WidgetReference
While the syntax for listening a provider in widgets and providers would now look similar with both being
ref.watch(myProvider)
, it is important to note thatProviderReference
andWidgetReference
are distinct objects.They are not interchangeable, and you could not assign
WidgetReference
toProviderReference
for example.Their main difference is,
ProviderReference
does not allow interacting withScopedProvider
s. On the other hand,WidgetReference
do.Similarly, it is entirely possible that in the future, some functionalities as added to one without being added to the other (such as #302 which allows
ProviderReference
to manipulate the state of its provider)Conclusion
That's it, thanks for reading!
As opposed to #246, one major difference is that this proposal is "compile safe" without having to rely on a custom linter.
The downside is that the syntax for reading providers becomes a tiny bit more verbose.
What do you think of the proposed change?
Feel free to leave a comment. You can also use 👍 and 👎 to express your opinion.
All feedbacks are welcomed!
The text was updated successfully, but these errors were encountered: