Closed rrousselGit closed 3 years ago
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 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?
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
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 https://github.com/rrousselGit/river_pod/issues/335#issuecomment-869850461 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 https://github.com/rrousselGit/river_pod/issues/335#issuecomment-920626096, 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 functional_widget
be updated to include ConsumerWidget
/HookConsumerWidget
? I previously used @hwidget
with useProvider
and it worked pretty well, cut down on a lot of boilerplate.
I wonder how many feel that useProvider
is simpler because they are used to it. A few months into the new syntax and I find the unification more intuitive.
I only have to understand ref.watch
and ref.read
rather than useProvider
and ref.watch
. It's one less concept I have to wrap my head around and it's the same amount of typing.
In many cases, we've got rid of hooks as they don't add much value vs. just calling ref.watch
directly. For example, we had a useApi()
which then became ref.watch(apiProvider)
. It's about the same amount of typing, but more explicit.
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 ConsumerWidget
/ConsumerHooKWidget
and do:
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 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 ConsumerWidget
.
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 provider
, this will override another
too. But having to specify the dependencies is tedious.
Hence the idea of using metaprogramming here. Where folks would write:
final provider = Provider((ref) => 'Hello')
final another = Provider((ref) => @watch(provider));
and that dependencies: {provider}
would be generated by the macro for you. That should fully fix the problem.
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 provider
package. What exactly is the motivation for not using something along the lines of context.watch(counterProvider)
? The syntax would be the same, except for the word context
, but one could argue that this would justify renaming the Provider
construction's parameter ref
to context
. This would also solve the problem with flutter_hooks
, as it would be possible to write useContext().watch(counterProvider)
. And it makes sense in cases where only a context
but no ref
is available. Also you don't need to change the widget base class (big win). In general it seems extremely convenient, and removes the need for metaprogramming in the build method.
I'm aware you mentioned that this is not feasible, perhaps I just misunderstood the architecture, could you elaborate further? I'd imagine if provider
can do it, so can riverpod
? This being a widely used package, I would personally place the importance of a clean way to use the package over how clean the implementation is.
I clearly see the advantages of riverpod
over provider
, but with the way things are moving now I would begrudgingly switch back.
I'm aware you mentioned that this is not feasible, perhaps I just misunderstood the architecture, could you elaborate further? I'd imagine if provider can do it, so can riverpod?
One of the very reason Riverpod is a separate package from Provider is because context.watch
isn't reasonably doable.
context.watch
works by using InheritedWidgets to rebuild widgets. ref.watch
isn't relying on InheritedWidgets, but rather a combination of "addListener" + "setState".
This is a necessary requirement for various reasons, be it fixing bugs with InheritedWidgets or adding support for new features or performance.
Using context
would require:
autoDispose
– because it isn't possible for InheritedWidgets to know when a listener is removed)family
– because widgets are incapable of conditionally listening to an InheritedWidgetmain.dart
like with package:provider
In comparison, using ConsumerWidget
instead of StatelessWidget
is a benign change.
@pikaju Seeing as it is infeasible to go back to the 'context' way of doing things, is there anything that we can do to make the transition easier for you? For example we have the riverpod_cli package that provides a migration tool to migrate automatically from the pre-1.0.0 riverpod to the latest dev version.
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 ref
, that parameter would now need to be drilled through all the hooks that need it. But again, it was a tradeoff that had to be made. I personally prefer passing this along since it's a more explicit dependency.
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 flutter_riverpod
, hooks_riverpod
, flutter_hooks
, and even the Flutter framework source code to see how things currently work under the hood, and spent a lot of time trying to think of possible improvements. I admit it is very tough. Prepare for a lot of rambling.
My main goal was to apply the "composition over inheritance" rule to both flutter_riverpod
as well as flutter_hooks
. Turning ConsumerState
into a ConsumerStateMixin
and simply not passing the ref
into the build function. I honestly prefer that over a new base class with new parameter sets, but it doesn't change much, and doesn't solve the problem of having an XxxYyyZzzWidget
in hooks_riverpod
, as @PiN73 called it, or in this case, an XxxYyyZzzWidgetMixin
. So I thought about changing flutter_hooks
to have a HookWidgetMixin
instead of a HookWidget
(bear with me). Unfortunately, that does not work because both packages replace the default Element
types with their own subtypes to implement their special behavior, making them somewhat mutually exclusive by nature, which, like you, I also didn't find a way around. I thought about creating a new package containing a compositional Element
type that packages like flutter_hooks
and flutter_riverpod
can depend on in order to harmonize with one another, but this is assuming there is a need for more widgets of this style.
Eventually I thought it would be better to just create a subtype of InheritedWidget
that is capable of detecting when a listener is removed, so that we can roll with context.watch
, but when I saw how the Flutter framework implements dependency removal, I flipped my table. Perhaps some Flutter pull requests would help to go the context.watch
route, what do you think?
TLDR: You guys did the best you could, I surrender. All hail our lord and savior, metaprogramming. If at all possible, please try adding useWidgetRef
.
@TimWhiting I appreciate the effort, but I'm personally not a big fan of automatic code conversion tools.
@venkatd Keep in mind that flutter_riverpod
is already detached from riverpod
. I'm only looking for ways to improve the Flutter/hooks bindings.
Believe me, I spent a lot of time trying to get BuildContext
to work.
I'm open to suggestions, but I haven't found any approach that wouldn't have fundamental flaws.
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 ConsumerWidget
, I don't see that as a real issue.
Flutter already comes with a lot of Widget type; far more than 3 that is. Adding one or two more seem innocent to me.
I wouldn't place too much hope in metaprogramming though.
While I would love to be able to do final value = @watch(provider)
and remove ConsumerWidget
/HookWidget
, the Dart team doesn't seem too interested in the use-case.
They haven't straight-up rejected it, but I highly doubt that the initial release will allow us to do that.
Time to finally close this issue. Thanks for everyone's participation!
This RFC is a follow-up to https://github.com/rrousselGit/river_pod/issues/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 https://github.com/rrousselGit/river_pod/issues/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 https://github.com/rrousselGit/river_pod/pull/302 which allows
ProviderReference
to manipulate the state of its provider)Conclusion
That's it, thanks for reading!
As opposed to https://github.com/rrousselGit/river_pod/issues/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 :+1: and :-1: to express your opinion. All feedbacks are welcomed!