River_pod: Provider a way to merge AsyncValue together

Created on 31 Jul 2020  路  17Comments  路  Source: rrousselGit/river_pod

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)
enhancement

Most helpful comment

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'),
);

All 17 comments

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}');

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?

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'),
);

Sorry for the misunderstanding about useProvider.

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

@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

AsyncValue.merge does not exist yet.

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,
        );
      },
    );
  }
}

is works:

AsyncValue<int> value;

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

@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 ?

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

Remove the || or change them into &&

replace all || with &&

tanya riverpod

Only use 1 param

tanya riverpod only one

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.

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

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(),
    ),
  );
}

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.

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.

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

Was this page helpful?
0 / 5 - 0 ratings