rrousselGit / riverpod

A reactive caching and data-binding framework. Riverpod makes working with asynchronous code a breeze.
https://riverpod.dev
MIT License
6.17k stars 943 forks source link

Feature request: Auto-dispose family provider that uses a type as key with its return type being parametrised by the key-type #351

Closed derolf closed 3 years ago

derolf commented 3 years ago

So far, we can have auto-dispose family providers with a fixed return type.

But, let's look at the following example:

class Car {}

class BMW extends Car {}

class Audi extends Car {}

class CarRepository {
  final all = StateProvider<List<Car>>((ref) => [BMW(), Audi(), BMW()]);

  // get all cars of a specific type
  AutoDisposeProvider<List<T>> byType<T>() => ???
}

final carRepository = CarRepository();

void build(BuildContext context) {
  final bmws = useProvider(carRepository.byType<BMW>());
}

I was thinking how to implement byType in a generic way and came up with the following (imperfect) solution:

///
/// Equality is implemented to check the passed `keyType`.
///
class _AutoDisposeProviderTypeFamilyKey {
  _AutoDisposeProviderTypeFamilyKey(this.keyType, this.create);

  final Type keyType;
  final dynamic Function(ProviderReference ref) create;

  @override
  int get hashCode => keyType.hashCode;

  @override
  bool operator ==(Object other) => other is _AutoDisposeProviderTypeFamilyKey && other.keyType == keyType;
}

Type _typeOf<T>() => T;

///
/// A family of `AutoDisposeProvider`s that are keyed by their type.
///
class AutoDisposeProviderTypeFamily {
  AutoDisposeProviderTypeFamily();

  /// manages the actual objects, and delegates to `create` in `_AutoDisposeProviderTypeFamilyKey`
  final _map = Provider.autoDispose.family<dynamic, _AutoDisposeProviderTypeFamilyKey>((ref, key) => key.create(ref));

  ///
  /// Lookup the value by the given `Key` type. `create` is used to construct the `value` if the key did not exist so far.
  ///
  AutoDisposeProvider<Value> call<Key, Value>(Value Function(ProviderReference ref) create) {
    // we are getting the actual object from `_map` but need to wrap it into another AutoDisposeProvider to apply the cast
    return Provider.autoDispose<Value>(
      (ref) => ref.watch<dynamic>(_map(_AutoDisposeProviderTypeFamilyKey(_typeOf<Key>(), create))) as Value,
    );
  }
}

Now, we can finish CarRepository:

class CarRepository {
  final all = StateProvider<List<Car>>((ref) => [BMW(), Audi(), BMW()]);

  final _byType = AutoDisposeProviderTypeFamily();

  // get all cars of a specific type
  AutoDisposeProvider<List<T>> byType<T>() => _byType<T, List<T>>((ref) => ref.watch(all).state.whereType<T>().toList());
}

@rrousselGit, probably you can give some advice how to do it better...

rrousselGit commented 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.

derolf commented 3 years ago

Actually, the reason is that I have a potentially big union and don't want to provide variables for all of them individually.

rrousselGit commented 3 years ago

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.

derolf commented 3 years ago

Actually, I thought leveraging the family gives me caching for free...

rrousselGit commented 3 years ago

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.

TimWhiting commented 3 years ago

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>();
}
rrousselGit commented 3 years ago

The fact that you need a "cast" in the build method mostly defeats the purpose imo

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

TimWhiting commented 3 years ago

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>();
}
rrousselGit commented 3 years ago

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>

derolf commented 3 years ago

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.

TimWhiting commented 3 years ago

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>());
}
derolf commented 3 years ago

Actually, I wanted an abstraction of what I did in the initial post, potentially to be included in the riverpod package.

rrousselGit commented 3 years ago

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.

derolf commented 3 years ago

Closing...