Skip to content

Instantly share code, notes, and snippets.

@gruvw
Last active November 2, 2025 20:21
Show Gist options
  • Select an option

  • Save gruvw/61c5e5330eb585220fd90eb73ee38d34 to your computer and use it in GitHub Desktop.

Select an option

Save gruvw/61c5e5330eb585220fd90eb73ee38d34 to your computer and use it in GitHub Desktop.
import 'dart:math';
import 'package:riverpod/riverpod.dart';
final random = Random();
Stream<bool> createStream() async* {
final duration = Duration(milliseconds: 1000 + random.nextInt(1000));
await Future.delayed(duration);
yield true;
await Future.delayed(duration);
yield true;
await Future.delayed(duration);
yield false;
}
final streamProvider1 = StreamProvider((ref) => createStream());
final streamProvider2 = StreamProvider((ref) => createStream());
final streamProvider3 = StreamProvider((ref) => createStream());
// will create synthetic loading states
final faultyNonProvider = FutureProvider((ref) async {
final value = await ref.watch(streamProvider1.future);
return !value;
});
// won't create synthetic loading states
final correctNonProvider = Provider<AsyncValue<bool>>((ref) {
final valueAsync = ref.watch(streamProvider1);
return valueAsync.whenData((value) => !value);
});
// will create synthetic loading states
final faultyCombineProvider = FutureProvider((ref) async {
final value1 = await ref.watch(streamProvider1.future);
final value2 = await ref.watch(streamProvider2.future);
final value3 = await ref.watch(streamProvider3.future);
return (value1 && value2) || value3;
});
// combine async values without synthetic loading states, requires nesting
final verboseCombineProvider = Provider<AsyncValue<bool>>((ref) {
final value1Async = ref.watch(streamProvider1);
return value1Async.when(
skipLoadingOnRefresh: false,
data: (value1) {
final value2Async = ref.watch(streamProvider2);
return value2Async.when(
skipLoadingOnRefresh: false,
data: (value2) {
final value3Async = ref.watch(streamProvider2);
return value3Async.whenData((value3) {
return (value1 && value2) || value3;
});
},
error: (error, stackTrace) => AsyncError(error, stackTrace),
loading: () => AsyncLoading(),
);
},
error: (error, stackTrace) => AsyncError(error, stackTrace),
loading: () => AsyncLoading(),
);
});
// extension to reduce verbosity
extension MapData<T> on AsyncValue<T> {
AsyncValue<E> mapData<E>(AsyncValue<E> Function(T value) data) {
return when(
skipLoadingOnRefresh: false,
data: data,
error: (error, stackTrace) => AsyncError(error, stackTrace),
loading: () => AsyncLoading(),
);
}
}
// combine async values without synthetic loading states, still requires nesting
final nestedCombineProvider = Provider<AsyncValue<bool>>((ref) {
final value1Async = ref.watch(streamProvider1);
return value1Async.mapData((value1) {
final value2Async = ref.watch(streamProvider2);
return value2Async.mapData((value2) {
final value3Async = ref.watch(streamProvider2);
return value3Async.whenData((value3) {
return (value1 && value2) || value3;
});
});
});
});
// ISSUE: how to properly combine async values without synthetic loading states?
// when one is loading, don't continue execution of create and state should be loading
// when one is error, don't continue execution of create and state should be the error
// final correctCombineProvider = Provider<AsyncValue<bool>>((ref) { // or maybe FutureProvider<bool>
// final value1Async = ref.watch(streamProvider1);
// final value2Async = ref.watch(streamProvider2);
// final value3Async = ref.watch(streamProvider3);
// return (value1Async && value2Async) || value3Async; // ?
// });
// and with family providers as well?
// final correctFamilyCombineProvider = Provider<AsyncValue<bool>>((ref) {
// final value1Async = ref.watch(streamProvider1);
// final value2Async = ref.watch(streamProvider2(value1)); // ?
// final value3Async = ref.watch(streamProvider3(value2)); // ?
// return (value1Async && value2Async) || value3Async; // ?
// });
// unwrap solution: throwing loading states and errors
// extension workaround experiment
class LoadingState {}
extension Unwrap<T> on AsyncValue<T> {
T unwrap() => when(
data: (d) => d,
error: (e, s) => Error.throwWithStackTrace(e, s),
loading: () => throw LoadingState(),
);
}
Provider<AsyncValue<StateT>> asyncValueProvider<StateT>(
StateT Function(Ref) create,
) => Provider((ref) {
try {
return AsyncData(create(ref));
}
// should preserve previous state below, must be done in riverpod internally
on LoadingState {
return AsyncLoading();
} catch (e, s) {
return AsyncError(e, s);
}
});
// use unwrap to combine async values without synthetic loading states
final andUnwrapProvider = asyncValueProvider<bool>((ref) {
final asyncValueA = ref.watch(streamProvider1);
final asyncValueB = ref.watch(streamProvider2);
final asyncValueC = ref.watch(streamProvider3);
return (asyncValueA.unwrap() && asyncValueB.unwrap()) || asyncValueC.unwrap();
});
void main() {
final container = ProviderContainer();
// change provider to see the different behaviors
final provider = andUnwrapProvider;
container.listen(provider, (_, state) => print(state), fireImmediately: true);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment