Currently, there's no mechanism to filter updates on the consumer level.
There _is_ updateShouldNotify on the provider level, but it's used for a completely different reason.
It'd be cool to have a way for a widget to explicitly not update when if a field other then the one used changes.
This cannot be done by Provider.of<T>, but probably doesn't matter.
On the other hand, Consumer can.
There are two main ways to implement such feature:
dart
Consumer<Foo>(
shouldRebuild: (prev, current) => prev.value != current.value,
builder: (_, Foo foo, __) => Text(foo.value),
)
== to not trigger updates when nothing changed.dart
Selector<Foo, String>(
selector: (foo) => foo.value,
builder: (_, String value, __) => Text(value),
)
The question is: which method should provider implement?
updateShouldNotifyConsumer without changeChangeNotifier implementation).shouldRebuild method, by for example forgetting to update the method after editing the content of builder.I don't like this approach. But it's simple
The complete opposite of the other solution, which I prefer.
ChangeNotifier too== is correctly implemented (it can be generated or made generic), then it is almost guaranteed that it works, even after changing builder.built_value) to generate the == of the "picked values".Consumer as is because it requires an extra generic type. As such, we have two different implementation path:
Since Consumer has variadic variations, we can use variants other than Consumer<Foo>() to implement a selector:
Consumer2<Consumed, Selected>.selector(
selector: (Consumed value) => Selected(value.foo),
builder: (context, Selected selected, child) => Widget,
)
This works fine. But it's:
Consumer() won't have that constructorConsumer2 wouldn't actually consume two objects, but one that is reduced.We can make a completely new widget, that'd work like Consumer, but with an extra argument:
Selector<Consumed, Selected>(
selector: (Consumed value) => Selected(value.foo),
builder: (context, Selected selected, child) => Widget,
)
It's easier to discover & less confusing. But we end up having Consumer and Selector fighting against each other.
I would vote for the "selector"/"reducer" approach since it makes things less error-prone as you mentioned. It's very likely to modify the builder method and forget to modify shouldRebuild, so selector would solve that.
I also agree that Consumer2 with a named constructor would be confusing, especially since it serves a different purpose than the one actually intended by Consumer2. Adding the Selector widget would be a better choice. Consumer and Selector won't be actually "fighting" against each other: they each have their own use cases and developers can choose the one to use according to their needs.
However, I have a small question: what if we want the Selector to update according to two or more values? For example if I have a User and I'm using the firstName and lastName properties to build a Text widget. Obviously there are some workarounds, like using nested selectors or creating an object that wraps around these two properties, but will the provider package address these use-cases by creating something like MultiSelector for example?
Obviously there are some workarounds, like using nested selectors or creating an object that wraps around these two properties, but will the provider package address these use-cases by creating something like MultiSelector for example?
How would a MultiSelector work?
I just realized that I didn't take into consideration the fact that we're passing the selected value to the builder method, I was only thinking about the values in the selector. However, I got an idea for it but I'm not sure if it would work.
We could use a Map<String, dynamic> to store the selected values and loop over the map's values to check if a rebuild is necessary (this is the part I'm not sure will work). Here's how it could look like:
MultiSelector<User>(
selector: (User user) => {
"firstName": user.firstName,
"lastName": user.lastName,
},
builder: (context, Map<String, dynamic> selected, child) {
return Text("${selected['firstName']} ${selected['lastName']}");
}
)
Another negative point about it is that it doesn't look so clean, especially inside the builder method.
Since I'm new to getting involved in such discussions, today I learned to think about what I'm planning to say before I say it 馃槀
Haha, no problem.
As for Map, I don't like it. It's not type-safe, so can cause issues.
But peoples can use a Tuple instead (from tuple)
That Tuple class overrides == already, so it should work as expected.
Selector<Foo, Tuple2<String, int>>(
selector: (_, foo) => Tuple2(foo.myString, foo.myInt),
builder: (_, tuple, __) {
print(tuple.item1);
print(tuple.item2);
return Something();
}
)
It may even be part of the language at some point.
This is a form of the "equality is always from a perspective" problem that is tangentially discussed over at https://github.com/dart-lang/sdk/issues/26703.
What Consumers need is a way to test the equivalence of two instances from its own perspective. There isn't a simple solution to this, but adding a shouldRebuild parameter speaks directly to the problem.
The Selector approach seems a bit verbose to select a single property of the object. Also what if I want to have two Selectors that filter on String for instance? The Tuple approach is clever, but runs into the same problem of requiring distinct type combinations for selected values.
At what point should the user be designing their providers differently? Should the values filtered by a consumer just be a separate type to be provided? Allowing Consumers to choose what they rebuild on at all enables users to add one monolithic provider near the root of the tree and just filter at the consumer level.
You could also let users pass a list of Strings or Symbols to be tested for equality, and then compare them in noSuchMethod, if you wanted to watch the world burn.
The Selector approach seems a bit verbose to select a single property of the object. Also what if I want to have two Selectors that filter on String for instance?
Do you have an example? I don't quite understand.
There isn't a simple solution to this, but adding a
shouldRebuildparameter speaks directly to the problem.
Note that most users of provider use it for a scoped_model-like architecture, using ChangeNotifier, in which case shouldRebuild won't work.
From your example above, the selector is only able to choose one value to rebuild on. It's not clear whether Selected here is a user defined type or will be part of the library.
Selector<Consumed, Selected>(
selector: (Consumed value) => Selected(value.foo),
builder: (context, Selected selected, child) => Widget,
)
I don't understand why shouldRebuild doesn't work the same for all providers, but then again I don't see how it would work at all. Would it require changing Consumer to a StatefulWidget?
Selected is user defined.
The documentation would recommend Tuple, or to use built_value to make our own.
shouldRebuild doesn't work for ChangeNotifier subclass because we don't actually change the instance of the ChangeNotifier. It's always the same, but mutated.
As such both the "previous" and the "current" variable passed to shouldRebuild would always be the same.
+1 to Selector as a "selector"/"reducer"
About MultiSelector in order to select 2 or more values, I think that is not necessary if you use a ViewModel, for example:
class UserViewModel extends Equatable {
final String firstName;
final String lastName;
UserViewModel(User user) :
firstName = user.firstName,
lastName = user.lastName,
super([firstName, lastName]);
}
Selector<User, UserViewModel> (
selector: (User user) => UserViewModel(user),
builder: (_, userViewModel view, __) {
return Text("${view.firstName} ${view.lastName}");
}
)
I am looking some samples using Provider (learning how works) and found the following widget in provider shopper sample:
class _AddButton extends StatelessWidget {
final Item item;
const _AddButton({Key key, @required this.item}) : super(key: key);
@override
Widget build(BuildContext context) {
var cart = Provider.of<CartModel>(context);
return FlatButton(
onPressed: cart.items.contains(item) ? null : () => cart.add(item),
splashColor: Theme.of(context).primaryColor,
child: cart.items.contains(item)
? Icon(Icons.check, semanticLabel: 'ADDED')
: Text('ADD'),
);
}
}
If I don't understand wrong, if an Item is added or removed from CartModel ALL _AddButton are rebuilded.
If we use Selector we could make the following:
class _AddButton extends StatelessWidget {
final Item item;
const _AddButton({Key key, @required this.item}) : super(key: key);
@override
Widget build(BuildContext context) {
return Selector<CartModel, bool> (
selector: (CartModel cart) => cart.items.contains(item),
builder: (_, bool containsItem, __) {
return FlatButton(
onPressed: containsItem ? null : () => Provider.of<CartModel>(context, listen:false).add(item),
splashColor: Theme.of(context).primaryColor,
child: containsItem
? Icon(Icons.check, semanticLabel: 'ADDED')
: Text('ADD'),
);
}
);
}
}
Now _AddButton rebuilds FlatButton if its Item is added/removed from CartModel only.
@rrousselGit , do you consider this as a good use case for Selector?
Definitely, yes.
This reminds me a lot of .NET's INotifyPropertyChanged infrastructure. That may be something to look at.
@kjeremy I'm not familiar with it, but from my understanding, it's closer to ChangeNotifier. Am I right?
It's similar but allows you to discriminate which property (getter/setter) of a .NET class has changed. For example you can have something like (pseudo c#):
class C : NotifyPropertyChanged // Implements INotifyPropertyChanged and provides SetPropertyChanged
{
private int _foo;
public int foo {
get => _foo;
set => { SetPropertyChanged(&foo, value)} // Raises `PropertyChanged` event with "foo"
}
private int _bar;
public int bar {
get => _bar;
set => { SetPropertyChanged(&_bar, value)} // Raises `PropertyChanged` event with "bar"
}
}
And the subscriber does something like this:
void sub() {
var c = new C();
// Subscribe to a c but only care about changes to `foo` property
c.PropertyChanged += (what) {
if (what == "foo") {
// Do something with c.foo
}
};
}
If you look at .NET's databinding for WPF/XAML it's based on this (and in that case the subcription logic is autogenerated).
I think I've actually approached Provider from the mindset of .NET and so am fighting it a bit so this issue should help. My perception is that most of Provider acts on types. For instance if I have streams of latitudes and longitudes represented as doubles it makes more sense in the provider world to create new Latitude and Longitude classes that just contain doubles and use StreamProvider<Latitude>/Provider<Latitude>.
Could Selector had Selector2, Selector3 and so on like Consumer? That could open a lot of possibilities.
Yes, that's planned.
Just to add some info, in C# we had to pass the changing property's String name to INotifyPropertyChanged, which was a pain when we needed to refactor/rename properties, but then C# introduced the [CallerMemberName] Attribute, which simplified this a lot. I am not aware of dart having an equivalent.
Dart has an open issue discussing it https://github.com/dart-lang/language/issues/251.
Technically speaking, that is unrelated to the discussion.
What you're talking about is an alternative to ChangeNotifier.notifyListeners(), to be field-specific.
That's out of the scope of provider, which doesn't care about how 3rd party objects work. It's the role of Flutter instead.
On the other hand, it's in the scope of provider to support these kinds of objects
Provider could support "aspects" through InheritedModel
Although this will drastically complexify the API.
That "aspect" thingy could be an alternative to the "shouldRebuild" method.
The benefits would be that, as opposed to "shouldRebuild", this would work for mutable objects like ChangeNotifier too.
On the other hand, it's:
InheritedWidget to InheritedModelSelector requires less code when using a Tuple.I just read the source code of Selector and it looks really interesting, thank you for the good work!
I just wonder if we really have to choose between having a Selector and a shouldRebuild inside Consumer, why can't we have both?
This will be easier and more readable to implement than a Selector when we depend on multiple values changing, without the need to use Tuple or built_value to assure our variables are immutable or behave as they are one.
I understand it's easier to misuse it, but with good documentation and examples of misusage, it can be a lot simpler to use for many cases than a Selector, particularly for special conditions where we don't just depend on equality of a previous/new value or that we just don't want/need the extra boilerplate to generate an immutable object.
Also, Selector only passes the single new value to builder, and not the entire consumed object. That's good for simple operations, but more complex ones will want the entire object. For example, a ChangeNotifier could have a isValid getter that combines multiple values to determine if a form is valid, but when we are rebuild it, we care about the actual data that is valid, so we can show it, not only the boolean that decides if the data is valid or not.
But a shouldRebuild doesn't even work with the most popular provider used.
Why not? And which is it? (I think it's the ChangeNotifierProvider but don't want to assume).
ChangeNotifier being mutable, the "previous" and "current" values passed to shouldRebuild would always be the same.
This require an architecture other than ChangeNotifier.
Yes, that's where C# helped, identifying who changed.
Maybe Selector providing the object as well could cover the cases that I have in mind (validation, where we depend on one variable to define if we change, but the content of the change is another variable).
@feinstein Can you give me an example?
I just realized I might be exaggerating, as we can always do a Provider.of<Foo>(context, listen: false) inside the Selector builder and get the entire object.
Most helpful comment
Definitely, yes.