Closed rrousselGit closed 5 years ago
One of the possible implementations could be:
class ProxyProvider<T, R> extends StatefulWidget
implements SingleChildCloneableWidget {
const ProxyProvider({
Key key,
this.builder,
this.updateShouldNotify,
this.child,
}) : super(key: key);
final R Function(BuildContext, T, R) builder;
final UpdateShouldNotify<R> updateShouldNotify;
final Widget child;
@override
_ProxyProviderState<T, R> createState() => _ProxyProviderState();
@override
ProxyProvider<T, R> cloneWithChild(Widget child) {
return ProxyProvider(
key: key,
builder: builder,
updateShouldNotify: updateShouldNotify,
child: child,
);
}
}
class _ProxyProviderState<T, R> extends State<ProxyProvider<T, R>> {
R value;
@override
void didChangeDependencies() {
super.didChangeDependencies();
value = widget.builder(context, Provider.of<T>(context), value);
}
@override
Widget build(BuildContext context) {
return Provider<R>.value(
value: value,
child: widget.child,
updateShouldNotify: widget.updateShouldNotify,
);
}
}
We may want to give the ability to use a provider other than Provider<R>.value
using a named constructor.
Ah, is this what you meant over at #44? I love this idea as it's a common use case and a clean API. From first glance, it doesn't look like there are perf implications.
I wonder what @jiaeric thinks.
Ah, is this what you meant over at #44?
It's related, but not exactly. My remark on #44 got me thinking about potential use-cases/flaws, and it here we are. 😄
I think that's an interesting idea. But I want to make sure that we don't solve an issue by introducing a bigger problem down the road.
The current example is relatively easy. I'll think of more complex scenarios with a combination of:
ChangeNotifierProvider
/similarsetState
or similar within the value builderThere are some questions that need to be answered first I think. Like "when do we dispose of the created value?" because some objects may be recreated while others may be mutated.
Yeah, this could get pretty gnarly before things are satisfactorily answered. If you agree, I would put this outside 2.0.0 scope.
I would put this outside 2.0.0 scope.
No problem! That's not a priority anyway, it's just fuel for discussions.
Coming back to this: what's the implication when a provider depends on multiple other providers? I don't have a use case at hand, but I can imagine it will crop up. Can we compose ProxyProviders inside each other?
Currently, providers can't depend on other providers. Not in a sane way at least.
Our "only" options are:
Consumer
. That works fine as long as the object is immutable and don't need to be disposed of – which is rare in the ChangeNotifier
architecture.Provider.of<Foo>(context, listen: false)
. Works fine if there's no update to do. It works with BLoC, but the ChangeNotifier
architecture suffers again.StatefulWidget
and handle everything ourselves. Tedious and error-prone. Can we compose ProxyProvider
Definitely!
MultiProvider(
providers: [
Provider<Foo>(...),
ProxyProvider<Foo, Bar>(
builder: (context, foo, previous) => Bar(foo),
),
ProxyProvider<Bar, Baz>(
builder: (context, bar, previous) => Baz(bar),
),
]
);
I assume we'll want variations of the ProxyProvider
that consumes 1-6 values from other providers.
Similarly, I think it'd be interesting to have an optional providerBuilder
argument.
ProxyProvider<Foo, Bar>(
builder: (context, foo, previous) => Bar(foo),
providerBuilder: (context, bar, child) => ChangeNotifier.value(value: bar, child: child),
child: <whatever>
)
and the default behavior of providerBuilder
would be
(context, value, child) => Provider.value(value: value, child: child),
I've tried to use ProxyProvider, but without providerBuilder
you have to ditch ChangeNotifierProvider
which I find easiest to use.
Is the following setup matching what you meant when suggested wrapping in Consumer? Assuming that DataService is just to inject methods and it doesn't have any actual state in it.
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Provider<DataService>.value(
value: DataService(NetworkService(), LocalService()),
child: Consumer<DataService>(
builder: (context, dataService, child) {
return MultiProvider(
providers: [
ChangeNotifierProvider.value(
notifier: CountriesViewModel(dataService),
),
],
child: child,
);
},
child: ....
);
}
}
What do you mean by:
but without providerBuilder you have to ditch ChangeNotifierProvider which I find easiest to use.
?
There's providerBuilder
in the current implementation available there: #59
And there's an example using it here
Assuming we go forward with ProxyProvider
, I'll add a few shorthands to make using ProxyProvider
with existing providers easier. Like ProxyChangeNotifierProvider
and such.
Is the following setup matching what you meant when suggested wrapping in Consumer?
Yes but that still fails.
Assuming that DataService
is immutable, CountriesViewModel
still isn't.
Using your code snippet, you'll easily lose your state when rebuilding.
Oh, I don't know how I missed it... Sorry for that...
Yeah, you are right about losing state... So do I understand correctly that withProxyProvider
it is handled because of using previous
in the builder like in the example you just mentioned?
And thank you for those explanations! You're the best!
it is handled because of using previous in the builder like in the example you just mentioned?
Yes. That previous
exists for mutable objects. Immutable ones usually won't care about this argument.
I've been playing with providers with inter-dependencies.
One example I currently have looks like this:
static Widget create(BuildContext context) {
final auth = Provider.of<AuthBase>(context);
return Provider<ValueNotifier<bool>>(
builder: (context) => ValueNotifier<bool>(false),
child: Consumer<ValueNotifier<bool>>(
builder: (context, valueNotifier, _) => Provider<SignInManager>(
builder: (context) =>
SignInManager(auth: auth, isLoading: valueNotifier),
child: Consumer<SignInManager>(
builder: (context, manager, _) => ValueListenableBuilder<bool>(
valueListenable: valueNotifier,
builder: (context, isLoading, _) => SignInPage(
manager: manager,
isLoading: isLoading,
),
),
),
),
),
);
}
This could be reduced to:
static Widget create(BuildContext context) {
final auth = Provider.of<AuthBase>(context);
return Provider<ValueNotifier<bool>>(
builder: (context) => ValueNotifier<bool>(false),
child: ProxyProvider<ValueNotifier<bool>, SignInManager>(
builder: (context, valueNotifier, _) =>
SignInManager(auth: auth, isLoading: valueNotifier),
child: Consumer<SignInManager>(
builder: (context, manager, _) => ValueListenableBuilder<bool>(
valueListenable: valueNotifier,
builder: (context, isLoading, _) => SignInPage(
manager: manager,
isLoading: isLoading,
),
),
),
),
);
}
Beyond that, it would be nice to use MultiProvider
as in https://github.com/rrousselGit/provider/issues/46#issuecomment-491381021.
After some experimentation, I ended up with something like this:
static Widget create(BuildContext context) {
final auth = Provider.of<AuthBase>(context);
return MultiProvider(
providers: [
Provider<ValueNotifier<bool>>(
builder: (_) => ValueNotifier<bool>(false)),
ProxyProvider<ValueNotifier<bool>, SignInManager>(
builder: (_, valueNotifier, _) =>
SignInManager(auth: auth, isLoading: valueNotifier),
),
],
child: Consumer<ValueNotifier<bool>>(
builder: (_, valueNotifier, _) => Consumer<SignInManager>(
builder: (_, manager, _) => ValueListenableBuilder<bool>(
valueListenable: valueNotifier,
builder: (_, isLoading, _) => SignInPage(
manager: manager,
isLoading: isLoading,
),
),
),
),
);
}
Does this look reasonable?
Also, it would be nice to simplify the 3 nested consumers I currently have.
Maybe something like a ProxyConsumer
?
In any case, I like the direction Provider
is taking, thanks a lot!
Also, it would be nice to simplify the 3 nested consumers I currently have.
There's a variation of Consumer
which consumes more than one value: Consumer2
, ..., Consumer6
ProxyProvider
would follow the same treatment. So in your example we may use ProxyProvider2
instead, which would listen to both AuthBase
and ValueNotifier<bool>
at the same time.
Otherwise SignInManager
isn't rebuilt when AuthBase
changes.
NIT if AuthBase
never changes, but it's cool to keep the providers reactive.
Is SignInManager
listening to the ValueNotifier<bool>
? If so, you're likely missing on a dispose
. Which means this usage of ProxyProvider
is likely invalid.
All of these combined, you may want that instead:
ProxyProvider2<ValueNoftifier<bool>, AuthBase, SignInManager>(
builder: (_, isLoading, auth, previous) {
final manager = previous ?? SignInManger();
manager.auth = auth;
manager.isLoading = isLoading;
return manager;
},
dispose: (_, manager) => manager.dispose(),
)
Alternatively can change Provider<ValueNotifier<bool>>
into ListenableProvider<ValueNotifer<bool>>
.
Which means that SignInManager
do not need to listen to the ValueNotifier
, and may, therefore, become immutable.
Which also means that we can replace our previous example with:
ProxyProvider2<ValueNoftifier<bool>, AuthBase, SignInManager>(
builder: (_, isLoading, auth, _) => SignInManger(auth, isLoading.value),
)
Do you have any example on how ProxyProvider2,3,4... should look?
ProxyProvider2
would be syntax sugar for:
ProxyProvider<Foo, Bar>(
builder: (context, foo, previous) {
final baz = Provider.of<Baz>(context);
return Bar(foo, baz);
}
)
@rrousselGit thanks for all the info!
In my case SignInManager
doesn't listen to ValueNotifier
, only sets the value.
However you're right about disposing the ValueNotifier
. I should be doing this in the dispose
closure of Provider<ValueNotifier<bool>>
.
So this is what I came up with:
return MultiProvider(
providers: [
Provider<ValueNotifier<bool>>(
builder: (_) => ValueNotifier<bool>(false),
dispose: (_, valueNotifier) => valueNotifier.dispose(),
),
ProxyProvider<ValueNotifier<bool>, SignInManager>(
builder: (_, valueNotifier, __) => SignInManager(auth: auth, isLoading: valueNotifier),
),
],
child: Consumer2<ValueNotifier<bool>, SignInManager>(
builder: (_, valueNotifier, manager, __) => ValueListenableBuilder<bool>(
valueListenable: valueNotifier,
builder: (_, isLoading, __) => SignInPage._(
manager: manager,
isLoading: isLoading,
),
),
),
);
I really like that I can use Consumer2
with types that have inter-dependencies. As in, SignInManager
takes ValueNotifier<bool>
as an input, but this is taken care of by the ProxyProvider
. So I can just list the types I need with ConsumerX
. Correct?
Hey guys,
Something unclear about the docs. I am beginning to see that from the example the valueNotifier
in ProxyProvider
is coming from the Provider
above it. However the docs mention nothing about this and the variables aren't named the same, there's no indication that the constructor or object instance is shared. The parameter is passed in lower camel case but it's not clear where it's coming from. It's completely confusing. Can we update the docs to explain what's going on here?
From experience looking at larger Flutter applications, it is relatively common to have some dependency between providers.
For example, we may have a
Provider<Configuration>
and aProvider<AuthentificationService>
where the latter depends on the former.But that's currently not something we can represent purely using
provider
package. Our only solution right now is to somehow ejectprovider
and handle everything ourselves like so:That's not good. It reintroduces the ability to make a circular object graph & forget to update/dispose of an object. We're loosing all the fancy securities that using widgets for DI brings.
The discussion on https://github.com/rrousselGit/provider/issues/44 made me realize that an advanced
Consumer
may be able to solve this issue.Naively we could think of doing:
... The problem is that we're doing
final bar = Bar(foo);
inside the build method ofConsumer
. That's not good either – we may have memory leaks. But that's something we can fix using a more advancedConsumer
, which I'll callProxyProvider
for now.The idea behind
ProxyProvider
is that it would work similarly to the default constructor ofProvider
, but also likeConsumer
as it would read values from other providers and pass them to thebuilder
.The same example using
ProxyProvider
would be:In that example, the
builder
ofProxyProvider
would be called on the first build; or wheneverFoo
changes.