riverpod icon indicating copy to clipboard operation
riverpod copied to clipboard

Provider a way to merge AsyncValue together

Open stargazing-dino opened this issue 3 years ago • 70 comments

Is your feature request related to a problem? Please describe. I'm refactoring a small app I have to use river_pods but one thing I keep wanting is a way to combine providers in a when or maybeWhen so they both get loaded at the same time in the case they're asynchronous.

Describe the solution you'd like

// Both userProvider and profileProvider are `StreamProvider`s
return useProvider2(userProvider, profileProvider).when(
  data: (user, profile) {
    // Logged in
  },
  loading: () {},
  error: (_, __) {}
);

Or, another example, all possible badges (pulled from a json file) and the user's current badges.

// userBadgesProvider is a StreamProvider and badgesProvider is a FutureProvider
return useProvider2(userBadgesProvider, badgesProvider).when(
  data: (userBadges, allBadges) {
    // Show list of a user's current badges in list of all possible badges
  },
  loading: () {},
  error: (_, __) {}
);

Describe alternatives you've considered I can do what I want by checking both the .data values on the providers to check if they're null but then I lose out on the error case of when:

final user = useProvider(userProvider).data;
final profile = useProvider(profileProvider).data;
final isLoggedIn = user != null && profile != null;

Or for the other example, I currently do this:

useProvider(userBadgesProvider).when(
  data: (userBadges) {
    final badges = useProvider(badgesProvider).maybeMap<List<Badge>>(
      data: (asyncData) => asyncData.data.value,
      orElse: () => [],
    );
  },
  loading: () {},
  error: (_, __) {},
),

Additional context hooks_riverpod: 0.6.0-dev

[✓] Flutter (Channel dev, 1.21.0-1.0.pre, on Mac OS X 10.15.5 19F101, locale en-US)
 
[✓] Android toolchain - develop for Android devices (Android SDK version 29.0.3)
[✓] Xcode - develop for iOS and macOS (Xcode 11.6)
[✓] Chrome - develop for the web
[✓] Android Studio (version 3.6)
[✓] Connected device (3 available)

stargazing-dino avatar Jul 31 '20 01:07 stargazing-dino

Once thing you can do is use is:

AsyncValue<int> first;
AsyncValue<int> second;

if (first is AsyncError || second is AsyncError) {
  return Text('error');
} else if (first is AsyncLoading || second is AsyncLoading) {
  return Text('loading');
}

return Text('${first.data.value} ${seconc.data.value}');

rrousselGit avatar Jul 31 '20 10:07 rrousselGit

Thank you for the example code!

This might be my own laziness for not separating them based on their immediate location in the UI but in one place in my code I call down three separate asynchronous resources. This is just what I had previously from my Provider implementation that felt natural. With three or more the code you posted still works but there would be even more checks and nested values.

What would be the reason against something like useProviderN?

stargazing-dino avatar Jul 31 '20 17:07 stargazing-dino

What would be the reason against something like useProviderN?

It wouldn't to what you want. You're looking at combining multiple AsyncValue into one. useProvider is completely unrelated to AsyncValue, it's just returning whatever the provider exposes.

What you're probably looking for is something like:

final AsyncValue<First> user = useProvider(firstProvider);
final AsyncValue<Second> profile = useProvider(secondProvider);

return AsyncValue.merge(
  data: (read) {
     return Text('${read(user).name} ${read(profile).name}');
  },
  loading: () => CircularProgressIndicator(),
  error: (err, stack) => Text('error'),
);

rrousselGit avatar Jul 31 '20 18:07 rrousselGit

Sorry for the misunderstanding about useProvider.

But yes, having that merge ability on the AsyncValue would be very nice to have

stargazing-dino avatar Jul 31 '20 18:07 stargazing-dino

@rrousselGit i have same case to merge multiple AsyncValue to one , if i implement your advice i don't have ability to get error information.

class WelcomeScreen extends StatelessWidget {
  static const routeNamed = '/welcome-screen';
  @override
  Widget build(BuildContext context) {
    return Consumer(
      (ctx, watch) {
        final tugas = watch(showAllTugas);
        final pelajaran = watch(showAllPelajaran);
        final dosen = watch(showAllDosen);
        if (tugas is AsyncError || pelajaran is AsyncError || dosen is AsyncError) {
          return Scaffold(body: Center(child: Text('Error Found')));
        } else if (tugas is AsyncLoading || pelajaran is AsyncLoading || dosen is AsyncLoading) {
          return Scaffold(body: Center(child: CircularProgressIndicator()));
        }
        return Scaffold(body: Center(child: Text('Success ')));
      },
    );
  }
}

Another solution is using AsyncValue.merge , but i not see merge method.

tanya riverpod

Version Package

flutter_riverpod: ^0.6.1

zgramming avatar Aug 23 '20 00:08 zgramming

AsyncValue.merge does not exist yet.

rrousselGit avatar Aug 23 '20 01:08 rrousselGit

So for now i can only get error information from multiple AsyncValue using nested when ?


class WelcomeScreen extends StatelessWidget {
  static const routeNamed = '/welcome-screen';
  @override
  Widget build(BuildContext context) {
    return Consumer(
      (ctx, watch) {
        final tugas = watch(showAllTugas);
        final pelajaran = watch(showAllPelajaran);
        final dosen = watch(showAllDosen);
        return tugas.when(
          data: (valueTugas) {
            return pelajaran.when(
              data: (valuePelajaran) {
                return dosen.when(
                  data: (valueDosen) {
                    return Text('Hore');
                  },
                  loading: null,
                  error: null,
                );
              },
              loading: null,
              error: null,
            );
          },
          loading: null,
          error: null,
        );
      },
    );
  }
}

zgramming avatar Aug 23 '20 02:08 zgramming

is works:

AsyncValue<int> value;

if (value is AsyncError<int>) {
  print(value.error);
  print(value.stack);
}

rrousselGit avatar Aug 23 '20 16:08 rrousselGit

@rrousselGit i don't why error and stack method not showing . When i try your example , i can see those method.

tanya riverpod

But when i implement it to my AsynValue<List<TugasModel>> not showing those method.

tanya riverpod 2

I mistake somewhere ?

zgramming avatar Aug 25 '20 12:08 zgramming

Because your if contains path where your value may not be an AsyncError

Remove the || or change them into &&

rrousselGit avatar Aug 25 '20 12:08 rrousselGit

replace all || with &&

tanya riverpod

Only use 1 param

tanya riverpod only one

zgramming avatar Aug 25 '20 12:08 zgramming

Eh Anyway it's not something I have control over. That's Dart, nor Riverpod

If you need to, you can make multiple ifs or you can use as to cast the variables.

rrousselGit avatar Aug 25 '20 12:08 rrousselGit

i see , thank's for your clarification. For now i think i can't get error and stack information without nested when

zgramming avatar Aug 25 '20 12:08 zgramming

About this issue:

I'm a bit mixed about https://github.com/rrousselGit/river_pod/issues/67#issuecomment-667294790, as this wouldn't be very performant and it could cause some confusion with hooks.

We could also have an AsyncValue.merge2 / AsyncValue.merge3 / AsyncValue.merge4 / ... which fuses AsyncValue<A> + AsyncValue<B> into AsyncValue<Tuple2<A, B>>. But we are losing the names and it could be confusing too, on the top of being a bit tedious to write.

A solution I am considering is to instead continue my previous experiment: https://github.com/rrousselGit/boundary

We would then write:

class Example extends ConsumerWidget {
  @override
  Widget build(context, watch) {
    AsyncValue<A> first;
    AsyncValue<B> second;

    A a = unwrap(first);
    B b = unwrap(second);

    return Text('$a $b');
  }
}

typically used this way:

Widget build(context) {
  return Scaffold(
    body: Boundary(
      loading: (context) => const Center(child: CircularProgressIndicator()),
      error: (err, stack) => Center(child: Text('Error $err'),
      child: Example(),
    ),
  );
}

rrousselGit avatar Sep 06 '20 21:09 rrousselGit

Considering Provider has something already similar to AsyncValue.mergeN I don't think it'd be too confusing for newcomers (all likely to come from Provider). That said I like the newer syntax using Boundary a lot.

stargazing-dino avatar Sep 08 '20 23:09 stargazing-dino

Considering Provider has something already similar to AsyncValue.mergeN

What are you referring to? Provider does not have AsyncValue. It has ProxyProviderN, but the equivalent is Riverpod's ref parameter

My main concern with AsyncValue.mergeN is the tuple. There is no standard Tuple in Dart, and this leads to quite an ugly syntax:

AsyncValue<Tuple3<int, String, double>> value;

value.when(
  data: (tuple) {
    return Text('${tuple.item1} ${tuple.item2} ${tuple.item3}');
  });
)

That itemN isn't very readable. We would need destructuring in Dart to make this syntax more reasonable.

rrousselGit avatar Sep 09 '20 00:09 rrousselGit

I don't think waiting for the Dart team to introduce native touples and destructuting would be the best option considering this proposal is a year old. The package Tuple on the other hand is at least maintained by google.dev so it's a fairly reliable a dependancy if you were to rely on it.

In any case, I'm all in favour of any solution you decide on

stargazing-dino avatar Sep 09 '20 01:09 stargazing-dino

I'm having the same problem now - what about a MultiAsyncProvider, which would take a list of either FutureProvider or StreamProvider or any type of provider which returns a type of AsyncValue, which would then produce either an AsyncError state if any items have error, a AsyncLoading state if any items are loading (and don't have error), and when all items are ready, would produce an List of AsyncData. Or would this be too specific? I would not want to clutter up the API's too much either so maybe this could be an optional third party package.

erf avatar Jan 09 '21 19:01 erf

I've just been using this for the meanwhile. Seems to be cover most use cases:

AsyncValue<Tuple2<T, R>> combineAsync2<T, R>(
  AsyncValue<T> asyncOne,
  AsyncValue<R> asyncTwo,
) {
  if (asyncOne is AsyncError) {
    final error = asyncOne as AsyncError<T>;
    return AsyncError(error.error, error.stackTrace);
  } else if (asyncTwo is AsyncError) {
    final error = asyncTwo as AsyncError<R>;
    return AsyncError(error.error, error.stackTrace);
  } else if (asyncOne is AsyncLoading || asyncTwo is AsyncLoading) {
    return AsyncLoading();
  } else if (asyncOne is AsyncData && asyncTwo is AsyncData) {
    return AsyncData(Tuple2<T, R>(asyncOne.data.value, asyncTwo.data.value));
  } else {
    throw 'Unsupported case';
  }
}

I've only needed to go up to 4 async values at one time so that's what I left it at. Here's the gist. Note this requires tuple dep

stargazing-dino avatar Jan 09 '21 20:01 stargazing-dino

That only covers the case of two AsyncValue's, I was looking for a generic case of an array of values, but it should not be too hard to make.

erf avatar Jan 09 '21 20:01 erf

Would it make sense to make a common base class for FutureProvider and StreamProvider called AsyncProvider? Which both return an AsyncValue so it would be easier to listen to a set of these?

erf avatar Jan 09 '21 21:01 erf

I made a proof of concept in this gist to combine values as i suggested in a previous comment.

I also made a method in order to check if all providers are loaded.

erf avatar Jan 10 '21 01:01 erf

The trouble with a list is that you lose out on type safety. You have to know the position of each value to properly type it once you get the values out. Sorry too, I only showed the combineAsync2 but the gist I linked has up to combineAsync4. I'm not implying my code is a solution either but just a functional placeholder until something better comes along

stargazing-dino avatar Jan 10 '21 03:01 stargazing-dino

I was thinking something like rxdart's combineLatest or Future.wait - it's the same order out (result) as you put in. Type is dynamic so you just set it to the correct type. But not sure how you should handle errors if more than one, maybe just the first one you encounter. Also not sure how to get data for all the various states. I just thought i add some thoughts so the maintainer might get some ideas.

erf avatar Jan 10 '21 04:01 erf

I made a repositiory as a package combined_provider if you want to check out.

erf avatar Jan 11 '21 20:01 erf

@erf I see where you're coming from now and I disagree with the need for the combineProviders you suggest. If you want a value from a Provider that isn't a StreamProvider or a FutureProvider you don't gain anything by having it inside a combineProvider. In fact, you lose type safety and considering riverpod promises to be Compile safe that's a big deal.

If you want a value from a Provider, StateProvider or ChangeNotifierProvider you can get them in build without doing anything special unlike async Providers

class Example extends ConsumerWidget {
  @override
  Widget build(BuildContext context, reader) {
    final myChangeNotifier = reader(myChangeNotifierProvider);
    final myState = reader(myStateProvider);

    // 
  }
}

If you need to filter rebuilds, then you can move all of your watch's to inside a Consumer:

class Example extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Consumer(
      builder: (context, watch, child) {
        final myChangeNotifier = watch(myChangeNotifierProvider);
        final myState = watch(myStateProvider);

        //
      }
    );
  }
}

The reason async providers are special cases is the different states their values can be from loading, error and data but you can't easily bunch them up without (possibly deeply) nesting them to account for all the states.

class Example extends ConsumerWidget {
  @override
  Widget build(BuildContext context, reader) {
    return reader(myStream).when(
      data: (data1) {
        return reader(mySecondStream).when(
          data: (data2) => Text('$data1, $data2'),
          loading: loading,
          error: error,
        );
      },
      loading: loading,
      error: error,
    );
  }
}

stargazing-dino avatar Jan 11 '21 23:01 stargazing-dino

You are right that there is not really a need to use it to get non-async types, but it was more for the convenience of get a list of values regardless of the type, when any provider changes values.

But still, i think my solution should work for async providers just as in your solution here and you should get the expected type in your result. There is an example there you can try out.

Please let me know what you are missing from my solution, as it seem to solve the problem you initially asked about. The nested example you provided seem to be a different use case.

erf avatar Jan 11 '21 23:01 erf

In my case I started to use two helpers. For merge async values:

AsyncValue<List<T>> merge<T>(Iterable<AsyncValue<T>> values) {
  final List<T> data = [];

  for (final value in values) {
    if (value is AsyncLoading) {
      return AsyncLoading<List<T>>();
    }

    if (value is AsyncError) {
      return AsyncError<List<T>>(
        (value as AsyncError).error,
        (value as AsyncError).stackTrace,
      );
    }

    if (value is AsyncData) {
      data.add(value.data.value);
    }
  }

  return AsyncData<List<T>>(data);
}

And unwrap it's values:

extension Unwrap<T> on AsyncValue<T> {
  T unwrap([T defaultValue]) {
    return maybeWhen(data: (data) => data, orElse: () => defaultValue);
  }
}

And how I use:

final history = watch(historyProvider);
final favorites = watch(favoritesProvider);

return merge([history, favorites]).when(
  data: (_) {
    print(history.unwrap());
    print(favorites.unwrap());
  } ,
  // ...
);

feronetick avatar Jan 12 '21 13:01 feronetick

It's pretty much the same i do, except i watch in another provider. Also i check for Error before Loading.

erf avatar Jan 12 '21 14:01 erf

@rrousselGit What do you think about something like this:

final exampleModelProvider = AsyncProvider<ExampleModel>((ref) {
  final history = ref.unwrap(historyProvider);
  final favorites = ref.unwrap(favoritesProvider);
  
  return ExampleModel(
    history: history,
    favorites: favorites,
  );
});

//....

final AsyncValue<ExampleModel> model = watch(exampleModelProvider);

feronetick avatar Jan 26 '21 22:01 feronetick