riverpod
riverpod copied to clipboard
Missing documentation for how providers are compared / when they are updated
Describe what scenario you think is uncovered by the existing examples/articles
I cannot find any documentation on when a Provider will notify about a new value.
Describe why existing examples/articles do not cover this case
I am wondering about this case in particular:
final myAwesomeProvider = Provider.autoDispose<List<int>>((ref) {
final list = watch(fooProvider).list;
return list;
});
I want to know if myAwesomeProvider will update dependants when the list object reference (identical) changes or when the iterable equality changes (DeepCollectionEquality/ListEquality).
Additional context
Will myAwesomeProvider notify dependants when the previous value was [1, 2, 3] (instance: 123aasd) and the new value is [1, 2, 3] (instance: 23wqej)?
And will it notify when the previous value was [1, 2, 3] (instance: askj) and the new value is [3, 1, 2] (instance: askj)?
@rrousselGit Also, this is something that I genuinely could not figure out.
I did not manage to find (did not search long enough probably) the code in Riverpod that notifies watchers about a Provider change. Would be great to know what equality operations are used (if it is just == or some custom handling for iterables e.g.).
@rrousselGit I suppose this would be the code: https://github.com/rrousselGit/river_pod/blob/97807e3ee1ed01a39047f3db78e43e1e829b82c4/packages/riverpod/lib/src/provider.dart#L224
If that is the case, there is no Iterable / general collection support. Would it make sense to create a PR for it or what is the recommended approach?
Should I just wrap my list in a class that overrides == to compare the lists properly?
Should I just wrap my list in a class that overrides == to compare the lists properly?
I'd suggest doing that
It used to have a deep comparison. But I removed it because in reality, new list instance means new content 99% of the time, so that was unnecessary computing
Alternatively you can make a PR that adds an "updateShouldNotify" to Provider
Just as one data point, I was looking to use .select to bind to some filtered lists on the model, where a new list does not necessarily indicate new data:
List<TodoItem> get completed => _all.where((item) => item.isCompleted == true).toList();
List<TodoItem> get active => _all.where((item) => item.isCompleted == false).toList();
The examples you gave (List filtering) would be better solved with a dedicated provider for this:
final completedTodos = Provider((ref) {
final all = ref.watch(allTodos);
return all.where((todo) => todo.isCompleted).toList();
})
The benefit is, this would cache the computation. So reading the provider twice will not filter the list twice.
That's interesting, my worry with this approach is just scalability though like it seems like I'll end up with tons of providers, and they will all (potentially) reference each-other and it could be pretty spaghetti like, vs the clear encapsulation of just providing one model for each type of data, and that model deals with filtering/sorting whatever internally.
Thinking about it some more, I guess what I really don't like how it scatters the code around, .where((todo) => todo.isCompleted) seems like domain logic that fairly clearly belongs in the model, not inside of a provider somewhere.
it seems like I'll end up with tons of providers, and they will all (potentially) reference each-other and it could be pretty spaghetti like
I would agree with you if not for a voluntary restriction: Circular dependencies aren't allowed.
It is technically impossible to make a spaghetti dependency graph because Riverpod forces uni-directional data flow.
What it ultimately boils down to is property vs provider.
But I think declaring these kinds of providers as static final is a good pattern. Like:
class TodoItem {
static final all = Provider(...)
static final completedTodos = Provider(...);
}
Ah I see, ya looking at it that way makes more sense... I need to maybe think of it as a model of providers, and not providing a single model.
Since I'm still not totally sold on the trade-offs with these micro-providers, I ended up opting for the suggested approach, and making something like:
class EquatableList<T> with EquatableMixin {
EquatableList(this.items);
final List<T> items;
@override
List<Object?> get props => items;
}
Then I could have something like this, and bind to it efficiently:
EquatableList<TodoItem> get completed => EquatableList(_all.where((i) => i.isCompleted).toList());
I know this is a little wasteful due to the where calls, but hey, this is bread and butter for dart machine code :D
Rebuilds are working great:
https://user-images.githubusercontent.com/736973/138825016-a5afa72c-551e-4804-92d7-2e24e5f09f2c.mp4
@rrousselGit I tried to do the static providers thing, but for some reason I'm still getting rebuilds in both of my consumers.
I have something like this:
class TodosController extends ChangeNotifier {
static final activeItemsProvider = Provider((ref) {
final all = ref.watch(todosController).all;
return all.where((i) => !i.isCompleted).toList();
});
static final completedItemsProvider = Provider((ref) {
final all = ref.watch(todosController).all;
return all.where((i) => i.isCompleted).toList();
});
}
https://github.com/esDotDev/flutter_experiments/blob/master/riverpod_todo_with_changenotifier/lib/controller/todos_controller.dart
And I bind to them like:
Consumer(builder: (_, ref, __) {
debugPrint("Build Active");
List<TodoItem> items = ref.watch(TodosController.activeItemsProvider);
return ListView(children: items.map((i) => TodoItemRenderer(i)).toList());
})),
...
Consumer(builder: (_, ref, __) {
debugPrint("Build Completed");
List<TodoItem> items = ref.watch(TodosController.completedItemsProvider);
return ListView(children: items.map((i) => TodoItemRenderer(i)).toList());
}))
https://github.com/esDotDev/flutter_experiments/blob/master/riverpod_todo_with_changenotifier/lib/view/todos/todos_page.dart
But when I modify one of the lists, both consumers rebuild. Am I doing something wrong?
Using EquatableList, rebuilds are properly optimized:
EquatableList<TodoItem> get completed => EquatableList(_all.where((i) => i.isCompleted).toList());
EquatableList<TodoItem> get active => EquatableList(_all.where((i) => !i.isCompleted).toList());
If you check out the repo, it has both sets of code there and you can easily comment in the static provider method to test.
Hum, you're correct. You'd need a List that implements == here
Thanks! Haha ya I was scratching my head around how the providers could possibly work here...
Obviously we could cache the filtered lists if we needed too, but this seems like a nice easy to use approach for small-ish datasets.
fwiw, running a .where on 1million items is ~15ms on desktop and ~50ms on device. So anything under 10,000 items will not really be measurable at ~0.15 - 0.5ms.
I was also looking for an explanation of when providers update their listeners in the docs.