riverpod
riverpod copied to clipboard
Provider a way to merge AsyncValue together
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)
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.
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.
But when i implement it to my AsynValue<List<TugasModel>>
not showing those method.
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 &&
Only use 1 param
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
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.
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
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.
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?
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.
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
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.
I made a repositiory as a package combined_provider if you want to check out.
@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,
);
}
}
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.
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());
} ,
// ...
);
It's pretty much the same i do, except i watch in another provider. Also i check for Error before Loading.
@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);