-
-
Notifications
You must be signed in to change notification settings - Fork 518
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
Document good practices around "listen" flag #244
Comments
I'm proposing it here in a new issue because that discussion was actually a sidetrack of issue #188, which was closed for other reasons. |
Hello! To be honest I still don't understand the use case |
The use case is pretty much anywhere you could use child: Dependent<User>(builder: (cx, user, child) {
return RaisedButton(
child: Text(textFor('button_add')),
onPressed: () => submit(cx, user),
);
}), It's analogous to Consumer: child: Consumer<User>(builder: (cx, user, child) {
return Padding(
padding: const EdgeInsets.all(10.0, 0.0, 10.0 20.0),
child: Text("User: ${user.name}",
);
}), Both of these are merely convenience classes, making the code shorter and easier to understand. Here's the longer equivalent for the above code: child: Builder(builder: (context) {
final user = Provider.of<User>(context, listen: false);
return RaisedButton(
child: Text(textFor('button_add')),
onPressed: () => submit(context, user),
);
}),
child: Builder(builder: (context) {
final user = Provider.of<User>(context);
return Padding(
padding: const EdgeInsets.all(10.0, 0.0, 10.0 20.0),
child: Text("User: ${user.name}",
);
}), There are also times when you can't use Consumer, such as when you need to get the value from |
Ah poo. That wasn't the best example. Replace User with something dynamic like some object that the user is creating. |
Builder(builder: (cx, user, child) {
final user = Provider.of<User>(cx, listen: false);
return RaisedButton(
child: Text(textFor('button_add')),
onPressed: () => submit(cx, user),
);
}), I don't like that code. I think it's a bad practice to call IMO the call should be moved inside the callback: Builder(builder: (cx, user, child) {
return RaisedButton(
child: Text(textFor('button_add')),
onPressed: () {
submit(cx, Provider.of<User>(context, listen: false));
}
);
}), That's a lot safer because we can't mistakenly use the obtained value to build a |
Similarly, calling Even if |
Okay, so you're looking at changing provider so that this usage of Dependent would throw an exception when debugging because Provider can't tell if the
I didn't understand that. What if I want the following? Builder(builder: (context) {
final model = Provider.of<Model>(context);
return RaisedButton(
child: Text(model.count == 0 ? "Add First" : "Add Another"),
onPressed: () => submit(cx, model),
);
}), |
That's completely different since the value is used by My previous comment is to avoid: Widget build(context) {
final foo = Provider.of<Foo>(context, listen: false);
return Text('$foo');
} That should never happen. |
I was about to provide an example where the text is pulled from a value and the widget does not listen to changes in value. But then I realized that in this case, it doesn't matter whether So the code you provided isn't necessarily an error, but it's also not necessary to code it this way. It's safe to |
My biggest beef with Provider is the We'd have to be more conscious of the decision doing something like this, and the code would be clearer too: value = Provider.updatingValue<T>(context);
value = Provider.nonUpdatingValue<T>(context); |
Even if the part of the value used never changes, As the app scale, the "it never changes" may not be true anymore. This will cause a very hard to catch bug (the impacted widgets may not even be in the git diff). There are better alternatives, like |
I'm slowly starting to understand. I'm seeing some best practices:
|
I wonder if this is an argument for making the following changes:
Alternatively, add a more helpfully named method for returning non-listening values: value = Provider.nonUpdatingValue<T>(context) And maybe also deprecate |
Or maybe to emphasize that it can't be called while building: value = Provider.nonbuildingValue<T>(context); |
Exactly This is enhanced by the fact that the "build" method is extremely fast. It doesn't really matter if a widget rebuilds too often and won't cause any issue. The sanity impact of using
If folks want a custom method instead of We should be able to do: extension on Provider {
static nonBuildingValue<T>(BuildContext context) {}
}
final value = Provider.nonBuildingValue<T>(context); |
I'd think this depends on the application. But we can use that argument to drop the
Oh, right. I'm even doing that where I need a changing value at initialization.
I'm finding the Now I'm even more in favor of dropping value = Provider.buildingValue<T>(context);
value = Provider.nonBuildingValue<T>(context); That's only one more method that what we have now. It will break everything, but developers will be left a lot saner, and Provider's proper usage easier to understand.
I did think of that, coming from a Kotlin background. I already had the dart extension spec pulled up in another tab. It would be a pain in the butt to always have to include my own dart file to get them, though. |
I don't think there's any difference between both besides causing problems for #235 Instead of passing
You can combine it with the // my_provider.dart
export 'package:provider/provider.dart';
extension Foo on Provider {
static whatever() {}
} Then just import |
I was thinking of making a PR on flutter to make This way |
Hey that's awesome! I really am liking Dart more and more. I thought the nodejs module management system (and CommonJS) were fantastic improvements on the chaos of Java's package system, now I'm thinking that Dart has even one-upped that. |
Okay, I see that I need to abandon the Dependent proposal, but are there any other takeaways from this discussion requiring action? Maybe the docs need to say more about the usage of Consumer and |
I'll keep this open as a reminder to document the good practices of |
Do you also recommend these good practices of listen when using Provider + MobX? I would love to have more details on good practices in general, like for example:
|
No and no. Your store should by principle have no dependency on Flutter. |
So how would you deal with these things? By using global keys? I just see "simple" provider examples tutorials, but not more complex real ones, what is a shame. This kind of problems confuse me when thinking of the use of stores/models. As I see them, it is an opportunity to also split out the UI and the logic, so the state remains outside too. But still we need to use an stateful widget to create a controller for example. |
Usually a callback does the trick. Instead of: class MyModel {
MyModel(this.context);
final BuildContext context;
void myMethod() {
final anotherModel = Provider.of<AnotherModel>(context);
...
}
} we can have: class MyModel {
MyModel(this.getModel);
final T Function<T>() getModel;
void myMethod() {
final anotherModel = getModel<AnotherModel>();
...
}
} where is it created through: Provider(
create: (context) => MyModel(<T>() => Provider.of<T>(context)),
) We can also do: class MyModel {
MyModel(this.anotherModel);
final AnotherModel anotherModel;
void myMethod() {
...
}
}
Provider(
create: (context) => MyModel(Provider.of(context, listen: false)),
) or: class MyModel {
void myMethod(AnotherModel anotherModel) {
...
}
}
Provider(
create: (context) => MyModel(),
)
Provider.of<MyModel>(context).myMethod(Provider.of(context)); |
The code above doesn't seem useful for real-world models. What if I have other arguments in my constructor? class MyModel {
MyModel(this.anotherModel, this.id, this.title);
final int id;
final String title;
final AnotherModel anotherModel;
void myMethod() {
...
}
} Sure I can easily inject Provider(
create: (context) => MyModel(Provider.of<AnotherModel>(context), /* params here somehow? */),
) What would be a good approach using Provider? Or Provider is not good for this and I'm better off using a classic service locator? Would mixing a locator be a good idea? |
I don't get your question. |
Which part you didn't get exactly? Consider my immutable class Post {
final int id;
final String title;
final MyModel model;
Post({
@required this.id,
@required this.title,
@required this.model
});
} I wire up the Provider(
create: (context) => Post(model: Provider.of<MyModel>(context), id: ???, title: ???),
) But... how do I get hold of Does my question make sense? |
Can't you just pass whatever you want to the constructor? I don't see how that relates to provider |
I'm trying to inject a dependency in a model. Since Provider is touted as a DI solution for Flutter and the examples above show how to inject dependencies in "models" I thought for a moment it was possible. There's two problems:
So I'm now using a service locator to achieve this: class Post {
final int id;
final String title;
final MyModel model = ServiceLocator.of<MyModel>();
Post({
@required this.id,
@required this.title
});
} I simply call |
I still don't understand what you're trying to do. |
I want this to work: // ...
return GestureDetector(
onDoubleTap: () {
Post(id: 2, title: 'bar').model.doSomething();
},
child: Text(Post(id: 1, title: 'foo'))
);
|
That specific syntax is purposefully not possible because providers are scoped. If you're looking to do that, consider simply storing your |
Thanks! It's clear now.
Yes. Using something like Therefore it makes sense to use Provider in the UI layer (I really like |
I disagree with using both Ultimately it's your decision, but these are voluntary limitations and using globals or another service locator on the top of it is anti-pattern.
|
Would you mind explaining why it's "unhealthy" exactly? And why combining the approaches is an anti-pattern. I come from Ember.js which is a very well designed framework and injecting globally registered services is common practice. |
As such, providers are:
This grant in turn:
But combining For example, you could use have more than a single instance of your model at a time: Provider<Model>(
create: (_) => Model(),
child: MaterialApp(
routes: {
'/tutorial': (_) => Provider<Model>(
create: (_) => MockedModel(someValue: 42),
child: Home(),
),
),
'/': (_) => Home(),
},
),
) In this app, both the But that is not something you can represent using globals. Similarly, unidirectional data-flow is a very good tool to improve the scalability of your app. |
I agree those are practices to strive for, and this is why I use Provider in the UI layer. (Even if service locators are definitely overridable/testable) But dependency injection is also required outside the UI layer (in the model or service layers, that know nothing about widgets). How do you use Provider in that case? Actually – Do you have a multi-layered app example using Provider? Cause so far I have only found very short snippets, maybe this is why I'm having trouble understanding. |
Okay so what I understand is that all dependency injection should be configured in the UI layer with Provider, for "global" services something like the following: Widget build(context) {
return MultiProvider(
providers: [
Provider.value(value: Dio()),
ProxyProvider<Dio, Api>(
update: (_, dio, __) => Api(dio)
),
ProxyProvider<Api, PostsRepository>(
update: (_, api, __) => PostsRepository(api)
),
ProxyProvider<Api, CommentsRepository>(
update: (_, api, __) => CommentsRepository(api)
),
// long long list ...
],
child: MaterialApp( or would you use other tools for constructor injection? So that network-layer concerns are not exposed in the UI layer Another minor consequence of only using Provider is that these services can't be accessed before |
I've accidentally built a complete app thinking that Provider.of would never add a dependency, and that Consumer was the way to follow value updates, as it was suggested in #244 (comment). I still think it would be more intuitive to use widgets (Consumer) for building, and functions (.of) for/in actions. But I guess that ship has sailed :) I'll probably introduce a custom extension |
That's what the 4.1.0 is supposed to do, along simplifying |
Closing this, since these good practices are now enforced with the 4.1.0 extensions |
In issue #188 we discussed adding a class analogous to Consumer but which doesn't rebuild on value changes. It seemed to me that you, Remi, were against the idea because you had better plans. Those plans appeared to be contingent on Flutter making debugBuilding available in release mode. The Flutter team declined, so I'm here formally proposing a convenience class.
I'm using this class in my own code because sometimes it makes for cleaner code; that's all Consumer is doing, anyway. I'm calling it Dependent:
We also discussed naming this class NoRebuildConsumer and UnboundConsumer. Another possibility is NonlisteningConsumer. I'm kind of partial to Dependent because the dependent can still call
Provider.of<T>()
for some other type and therefore still rebuild.The text was updated successfully, but these errors were encountered: