Closed rrousselGit closed 3 years ago
Easier to read
I like most of it but I am not sure that I like the approach of a new ConsumerHookWidget
.
When working with hooks we have a "pre-understanding" that we can use the useSomething
statement to accomplish what we want, but the ConsumerHookWidget
implements a complete new class just for this.
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 HookWidget
to a ConsumerHookWidget
just to use the providers.
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 WidgetReference
as a parameter.
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 ref.listen
, then I need to add a hook for it.
I honestly like the proposal, only thing I don't really like is the necessity to use ConsumerWidget
instead of StatelessWidget
. For StatefulWidget
s it's okay as adding a mixin is an easy migration but having to rewrite all StatelessWidget
s as ConsumerWidget
s is going to be tedious. Would it be possible to have something like a ConsumerStateMixin
but for StatelessWidget
?
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. No promise though
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:
build
- Can be a little longer but can also be done using replace and Regex.context.read()
by ref.read()
, the first - Can also be done by replacing "context.read" by "ref.read".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.
The ConsumerHookWidget is mainly for maintainability, to avoid having to duplicate work.
The issue with the current approach is, if I add a
ref.listen
, then I need to add a hook for it.
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 context.read/refresh
and ref.read/refresh
(I think the only difference is ref
lookup delay, which is not even a minor issue), context.read/refresh
should be stayed as optional way. Depreciating it seems pointless to me.
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, ConsumerHookWidget
allows conditions/loops:
Column(
children: [
if (something)
Text(ref.watch(provider)),
Text('Hello world'),
],
)
This is not something doable with useProvider
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?
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 hooks_riverpod
, I’m a bit concerned about the ConsumerHookWidget
just like @RobertBrunhage said.
if the reason for it all is to have a WidgetReference
wouldn’t it be possible to return it through an hook:
Widget build(BuildContext context) {
final ref = useProviderRef()
final provider = ref.watch(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 useProvider
. Mainly for reasons of creating / composing custom hooks that use providers. I think that @dancamdev's proposal of having a hook that returns a WidgetReference
would be a good one if possible. However I also think that it would be nice to have the extra parameter in the build
method to allow reading / watching in loops / conditionals, with fewer lines of code. I also like that it makes it easier to refactor between ConsumerWidget
and ConsumerHookWidget
, possibly an analyzer plugin might be able to add those refactorings too.
// 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 ref.watch(provider)
in any conditional or loop. Again I'm no expert in how hooks work under the hood, so I may be wrong
@dancamdev
I'm not saying that you can't do it exactly your suggested way. In fact you can and you should be able to use the WidgetReference
in a loop just as you said.
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 ConsumerHookWidget
and ConsumerWidget
for when you don't need hooks. Having the api more consistent also makes the documentation simpler.
But I agree that we need a hook for getting a WidgetReference
for when you are not in the build method. Either way you should be able to use the WidgetReference
in a loop. Unless I'm wrong, and it's not possible to get a hook to do this? Remi can correct me.
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.
Everything is pretty much self explanatory. This way you can have context.container, context.refresh and etc.
context.watch
is not feasible.
On the plus side for hooks,
ConsumerHookWidget
allows conditions/loops:Column( children: [ if (something) Text(ref.watch(provider)), Text('Hello world'), ], )
This is not something doable with
useProvider
With this and with the answer you gave to my question, @rrousselGit, I declare myself in favor of the proposal! 😁
Generally, I think this is positive and is an improvement over the previous RFC. I agree with ConsumerHookWidget
but also agree with the suggestion from @dancamdev for a useProviderRef
hook for use inside of a HookWidget
.
Can I suggest that the following name changes:
WidgetReference
> ConsumerReference
– aligns with ConsumerWidget and ConsumerHookWidget and makes it clearer that it's not part of Flutter itself.ConsumerStateMixin
> ConsumerReferenceMixin
(or ConsumerReferenceStateMixin) – clearer what the mixin is addinguseProviderRef
> useConsumerRef
– assuming that a hook for accessing the reference is acceptedThere's a slight downside to the additional verbosity in consumer widgets and also that methods might now need to be passed both a build context and the proposed widget reference – I use private methods to avoid an unwieldy build method. I think this downside is worth it.
Would it be possible to get the reference from a context? Not sure if that would have the same issue as previously with not being able to have a context.watch method.
As a user of
hooks_riverpod
, I’m a bit concerned about theConsumerHookWidget
just like @RobertBrunhage said.if the reason for it all is to have a
WidgetReference
wouldn’t it be possible to return it through an hook:Widget build(BuildContext context) { final ref = useProviderRef() final provider = ref.watch(a); }
and have a plain HookWidget
this would also allow for conditionals and loops
I also agree with @dancamdev regarding useProviderRef
.
I like almost everything except the necessity to use a ConsumerWidget to read a provider in a callback of a StatelessWidget.
onPressed: () => ref.read(counterProvider).state++;
In the Consumer widget documentation you say:
[ConsumerWidget], a base-class for widgets that wants to listen to providers.
But in this case, we don't want to listen to nothing, we are simply incrementing the state.
I'm against useProviderRef
for technical reasons: Implementing it is basically no different from keeping useProvider
and adding a useProviderListener
So it defeats the point of having a ConsumerHookWidget
to remove code duplicates
I like almost everything except the necessity to use a ConsumerWidget to read a provider in a callback of a StatelessWidget.
onPressed: () => ref.read(counterProvider).state++;
In the Consumer widget documentation you say:
[ConsumerWidget], a base-class for widgets that wants to listen to providers.
But in this case, we don't want to listen to nothing, we are simply calling a function.
Note that this does not work if the provider is auto-disposed – you have to add a dummy watch in the build method if there might not be any other references.
About ref.read
instead of context.read
, one thing that this would allow is:
ref.read
would allow Riverpod to keep the provider obtained "alive" until the widget that called ref.read
is destroyed.
This is a common source of confusion with context.read
, but context.read
cannot maintain the state of providers because of the context API.
This is a separate issue though. And if we do that, ProviderReference.read
should be updated to also maintain the state of providers (it doesn't at the moment to behave the same way than context.read
to not confuse people)
I'm against
useProviderRef
for technical reasons: Implementing it is basically no different from keepinguseProvider
and adding auseProviderListener
So it defeats the point of having a
ConsumerHookWidget
to remove code duplicates
I see ConsumerHookWidget as allowing for standardisation of how to watch and read a provider's value. However, the useProvider hook is useful for hook composition, otherwise, we'd have to pass the reference in, which starts to get ugly.
I personally don't see the issue with:
T useMyProvider(ConsumerReference ref) {
final value = ref.watch(myProvider);
return value;
}
class Example extends ConsumerHookWidget {
@override
Widget build(context, ref) {
final value = useMyProvider(ref);
}
}
I personally don't see the issue with:
T useMyProvider(ConsumerReference ref) { final value = ref.watch(myProvider); return value; } class Example extends ConsumerHookWidget { @override Widget build(context, ref) { final value = useMyProvider(ref); } }
My point in using the useProviderRef()
hook would be to keep extending the plain HookWidget
, so there's no need to differentiate wether a Widget will be using a Provider or not. But I'm missing the whole picture probably, I'm not getting why this ConsumerHookWidget
+ ref passed to the build
method is such an improvement.
I'm against
useProviderRef
for technical reasons: Implementing it is basically no different from keepinguseProvider
and adding auseProviderListener
So it defeats the point of having a
ConsumerHookWidget
to remove code duplicates
@rrousselGit Trying to understand.
From a code duplication standpoint I understand you don't want to both. And I'm still okay with the proposal if we don't get a useProviderRef()
hook if it is going to cause bugs / problems with maintaining two implementations.
But could you not do something like this?:
Implement useProviderRef()
which returns a WidgetReference
or whatever it will be called, I like the proposal above to call it ConsumerReference
.
Then create ConsumerHookWidget
building off of useProviderRef
(not duplicating the code behind it):
abstract class ConsumerHookWidget extends StatefulWidget {
const ConsumerHookWidget({Key? key}) : super(key: key);
Widget build(BuildContext context, ConsumerReference ref);
@override
_ConsumerHookWidgetState createState() => _ConsumerHookWidgetState();
}
class _ConsumerHookWidgetState extends State<ConsumerHookWidget> {
@override
Widget build(BuildContext context) {
return HookBuilder((context) {
final consumerRef = useProviderRef();
return widget.build(context, consumerRef);
});
}
}
Apologies if I'm oversimplifying this or don't understand how Hooks and Riverpod is implemented.
I understand that this will still mean that for Hook's users there are two different ways to accomplish the same thing, which might be confusing to new users, so I'm fine if this isn't what ends up happening.
The issue is not about ConsumerHookWidget
vs useConsumerReference
It's about ConsumerWidget
vs useConsumerReference
.
ConsumerWidget
is unaware of hooks. As such, it cannot use useConsumerReference
. Similarly, there's no way to share code between them, since the life-cycles of hooks and stateful-widgets are different.
This could lead to subtle difference in behaviour between flutter_riverpod and hooks_riverpod and will require double the effort for any added feature (which is one of the reasons why hooks_riverpod supports useProvider(provider.select)
but flutter_riverpod doesn't yet)
On the other hand, ConsumerWidget
vs ConsumerHookWidget
would not have code duplication
Okay, thanks for explaining. It makes more sense. I'm in favor of the proposal.
Me too. I’m in favor of the proposal as it was originally written.
Thank you @rrousselGit!
This could lead to subtle difference in behaviour between flutter_riverpod and hooks_riverpod and will require double the effort for any added feature
That's all the argument I need. Now knowing that, I would happily accept the need to pass a ref through to a custom hook, if I were using HookWidget directly.
And more so going forward I would move away from having hook functions for accessing provider values. Hooks are for adding local state to a StatelessWidget. Riverpod providers are not and so we probably shouldn't be mixing the two.
@rrousselGit
Similar to Tim's idea to try and not maintain two separate implementations, wouldn't it be possible to maintain the hooks implementation and make the ConsumerWidget
use the hook implementation under the hood? From the user that doesn't want to use hooks perspective, that is an implementation detail.
Maybe you've already thought about this approach, but discarded for some reason. I guess a downside, if it could be seen as such, is that it uses hooks under the hood. I personally don't see it as a problem. I'm interested in hearing your thoughts on this.
This would be the implementation of the ConsumerWidget
as I see it:
abstract class ConsumerWidget extends StatefulHookWidget {
const ConsumerWidget({Key key}) : super(key: key);
Widget build(BuildContext context, ConsumerReference ref);
@override
ConsumerWidgetState createState() => ConsumerWidgetState();
}
class ConsumerWidgetState extends State<ConsumerWidget> {
@override
Widget build(BuildContext context) {
final ref = useProviderRef();
return widget.build(context, ref);
}
}
@davidmartos96 that would make everyone using flutter_Riverpod depend on flutter_hooks even if they do not use it.
@rrousselGit Yes, I know. I'd understand if you don't want to go that way.
@rrousselGit I'm in favor of the proposed changes too, your explanation on why useProviderRef()
wouldn't work as well makes a lot of sense, thanks for that.
Regarding having riverpod depend on hooks under the hood isn't advisable IMO, the fewer dependencies, the better.
I'm glad of the compile safety and easier switching between hooks and vanilla (syntax parity). Would this mean that existing youtube tutorials for Vanilla would apply to Hooks. Or might users now more readily assume similarity and potentially hit other syntax difference issues?
I'm glad of the compile safety and easier switching between hooks and vanilla (syntax parity). Would this mean that existing youtube tutorials for Vanilla would apply to Hooks. Or might users now more readily assume similarity and potentially hit other syntax difference issues?
@kevlar700 the syntax as proposed by Remi should be slightly different if I understood correctly, apart from that people should always refer to the docs for syntax related information, so they're sure it is up to date.
@kevlar700 @dancamdev To clarify, it is different than before - so old tutorials will be outdated, but it should achieve the syntax parity that @kevlar700 is talking about in the future. That is, there should be no differences in syntax and behavior between hooks and vanilla
As Remi said:
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(); } }
So unless you need to use hooks, there is no difference.
I am not too sure about losing the ability to 'read' a provider directly from a StatelessWidget and being forced to break out into a dedicated ConsumerWidget to access the proposed WidgetReference. I do however see the reasoning behind it, in keeping your code clean / "riverpod-like".
I must say I am intrigued by the idea of the ConsumerStateMixin, especially in terms of bringing the magic of Riverpod into existing code.
I love this proposal. I use hooks and riverpod extensively in my project. I would encourage everyone to look past the migration of your current project and think of the benefits this brings to a new person by unifying the syntax. I can also see the future benefit of not having to explain, to a new developer on my project, the many different ways you can get a provider's value and the way in which useProvider is intertwined with riverpod.
I like this proposal a lot! Unified syntax makes it easier to understand and use.
I've found myself starting to extend a ConsumerHookWidget already! 😄
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!