Closed derolf closed 3 years ago
That's a hard one.
I don't think there's a clean way to handle generic providers, because of one problem: generic constraints.
If we want to support generics, it is a given that we'd want to support T extends Something
. But that's hardly doable in a reusable way
Could you expand on why you need a generic provider?
Why not do:
abstract class Cars {
static final all = StateProvider<List<Car>>((ref) => [BMW(), Audi(), BMW()]);
static final bmws = Provider.autoDispose<List<BMW>>((ref) => ref.watch(all).whereType<BMW>());
static final audis = Provider.autoDispose<List<Audi>>((ref) => ref.watch(all).whereType<Audi>());
}
void build(BuildContext context) {
final bmws = useProvider(Cars.bmws);
}
After all, the list of Car subclasses is known. And the declaration of the provider is a one-liner.
Actually, the reason is that I have a potentially big union and don't want to provide variables for all of them individually.
But you don't really have the choice. You could otherwise easily have a memory leak or unperformant code.
Why do you need a byType
to begin with? Maybe the entire idea of having a provider for this filtering by type is incorrect.
For instance, why do:
final bmws = watch(Cars.byType<BMW>());
when you can do:
final bmws = watch(Cars.all).whereType<BMW>();
That achieves strictly the same thing.
Actually, I thought leveraging the family gives me caching for free...
Oh that's a fair point. But there's hardly a better solution.
Maybe use hooks and do:
final cars = watch(Cars.all);
final bmws = useMemoized(() => cars.whereType<BMW>(), [cars]);
Although I still believe the original Cars.bmw
is the way to go. It's not perfect, but that's still better than hacking a watch(byType<T>())
which is likely to have numerous issues.
This is what I would do:
class Cars {
static final all = StateProvider<List<Car>>((ref) => [BMW(), Audi(), BMW()]);
static final cars = Provider.autoDispose.family<List<Car>, Type>((ref, type) =>
ref.watch(all).state.where((c) => c.isSubtype(type)).toList());
}
extension Subtype on Object {
bool isSubtype<T>(T type){
return this is T;
}
}
//Consumer
extension CarTypeReader on Reader {
List<T> carType<T>(){
return this(Cars.cars(T)).cast<T>();
}
}
//Hooks
List<T> useCarType<T>() {
return useProvider(Cars.cars(T)).cast<T>();
}
// Usage
Widget build(BuildContext context, ScopedReader watch) {
// Consumer
final bmws = watch.carType<BMW>();
// Hooks
final audi = useCarType<Audi>();
}
The fact that you need a "cast
The point of extracting it in a provider is to have the provider behave as a cache. But that "cast" means we're creating a new list whenever the widget rebuild
Could you cast the list using as List<T>
do you think?
This is a case where the static metaprogramming language proposal would be helpful.
I think your hooks solution looks easiest, and could be separated out into a separate hook.
List<T> useCarType<T>(){
final cars = useProvider(Cars.all);
return useMemoized(() => cars.whereType<T>(), [cars]);
}
// Usage
Widget build(BuildContext context) {
final audi = useCarType<Audi>();
}
Could you cast the list using
as List<T>
do you think?
It won't because the list instance is a List<Car>
, not List<BMW>
My solution in the initial post was exactly to overcome the limitations of Dart by delegating the construction to the call site where I have access to the type.
To implement without a fancy delegate, we would require second order generics like:
typedef Create = Value<Key> Function<Key, Value<Key>>(Key key);
Then you could have something like Create<T, List<T>>
and resolve the delegate and move the code directly into the family-constructor.
So you could create your own 'family' by caching it yourself: Then you would have the type when the provider is being created. Not as easy as using a family, but more what you want it seems.
abstract class Cars {
static final all = StateProvider<List<Car>>((ref) => [BMW(), Audi(), BMW()]);
static final Map<Type, dynamic> _cache = {};
static AutoDisposeProvider<List<T>> cars<T>() {
if (!_cache.containsKey(T)) {
_cache[T] = Provider.autoDispose<List<T>>((ref) {
final allCars = ref.watch(all).state;
ref.onDispose(() {
_cache.remove(T);
});
return allCars.whereType<T>().toList();
});
}
return _cache[T] as AutoDisposeProvider<List<T>>;
}
}
void build(BuildContext context) {
final bmws = useProvider(Cars.cars<BMW>());
}
Actually, I wanted an abstraction of what I did in the initial post, potentially to be included in the riverpod package.
It's a lot of work for something fairly niche and with a reasonable workaround. Support in riverpod means support for all the combinations, so autoDispose vs non-autoDispose and all the providers.
Unless that's something a large number of people desires, I won't work on this.
Closing...
So far, we can have auto-dispose family providers with a fixed return type.
But, let's look at the following example:
I was thinking how to implement
byType
in a generic way and came up with the following (imperfect) solution:Now, we can finish
CarRepository
:@rrousselGit, probably you can give some advice how to do it better...