Websockets and synchronous execution
So far, we've only covered on how to create a Future
.
This is on purpose, as Future
s are the core of how Riverpod applications
should be built. But, Riverpod also supports other formats if necessary.
In particular, instead of a Future
, providers are free to:
- Synchronously return an object, such as to create a "Repository".
- Return a
Stream
, such as to listen to websockets.
Returning a Future
and returning a Stream
or an object
is quite similar overall. Think of this page as
an explanation of subtle differences and various tips for those use-cases.
Synchronously returning an object
To synchronously create an object, make sure that your provider does not return a Future:
int synchronousExample(Ref ref) {
return 0;
}
When a provider synchronously creates an object, this impacts how the object is consumed. In particular, synchronous values are not wrapped in an "AsyncValue":
Consumer(
builder: (context, ref, child) {
// The value is not wrapped in an "AsyncValue"
int value = ref.watch(synchronousExampleProvider);
return Text('$value');
},
);
The consequence of this difference is that if your provider
throws, trying to read the value will rethrow the error.
Alternatively, when using ref.listen
, the "onError" callback
will be invoked.
Listenable objects considerations
Listenable objects such as ChangeNotifier
or StateNotifier
are not supported.
If, for compatibility reasons, you need to interact with one of such objects,
one workaround is to pipe their notification mechanism to Riverpod.
/// A provider which creates a ValueNotifier and update its listeners
/// whenever the value changes.
ValueNotifier<int> myListenable(Ref ref) {
final notifier = ValueNotifier(0);
// Dispose of the notifier when the provider is destroyed
ref.onDispose(notifier.dispose);
// Notify listeners of this provider whenever the ValueNotifier updates.
notifier.addListener(ref.notifyListeners);
return notifier;
}
In case you need such logic many times, it is worth noting that the logic shared! The "ref" object is designed to be composable. This enables extracting the dispose/listening logic out of the provider:
extension on Ref {
// We can move the previous logic to a Ref extension.
// This enables reusing the logic between providers
T disposeAndListenChangeNotifier<T extends ChangeNotifier>(T notifier) {
onDispose(notifier.dispose);
notifier.addListener(notifyListeners);
// We return the notifier to ease the usage a bit
return notifier;
}
}
ValueNotifier<int> myListenable(Ref ref) {
return ref.disposeAndListenChangeNotifier(ValueNotifier(0));
}
ValueNotifier<int> anotherListenable(Ref ref) {
return ref.disposeAndListenChangeNotifier(ValueNotifier(42));
}
Listening to a Stream
A common use-case of modern applications is to interact with websockets,
such as with Firebase or GraphQL subscriptions.
Interacting with those APIs is often done by listening to a Stream
.
To help with that, Riverpod naturally supports Stream
objects.
Like with Future
s, the object will be converted to an AsyncValue
:
Stream<int> streamExample(Ref ref) async* {
// Every 1 second, yield a number from 0 to 41.
// This could be replaced with a Stream from Firestore or GraphQL or anything else.
for (var i = 0; i < 42; i++) {
yield i;
await Future<void>.delayed(const Duration(seconds: 1));
}
}
class Consumer extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
// The stream is listened to and converted to an AsyncValue.
AsyncValue<int> value = ref.watch(streamExampleProvider);
// We can use the AsyncValue to handle loading/error states and show the data.
return switch (value) {
AsyncValue(:final error?) => Text('Error: $error'),
AsyncValue(:final valueOrNull?) => Text('$valueOrNull'),
_ => const CircularProgressIndicator(),
};
}
}
Riverpod is not aware of custom Stream
implementations, such as
RX's BehaviorSubject
.
As such, returning a BehaviorSubject
will not expose the value
synchronously to widgets, even if already available on creation.
Disabling conversion of Stream
s/Future
s to AsyncValue
By default, Riverpod will convert Stream
s and Future
s to AsyncValue
.
Although rarely needed, it is possible to disable this behavior by wrapping
the return type in a Raw
typedef.
It is generally discouraged to disable the AsyncValue
conversion.
Do so only if you know what you are doing.
Raw<Stream<int>> rawStream(Ref ref) {
// "Raw" is a typedef. No need to wrap the return
// value in a "Raw" constructor.
return const Stream<int>.empty();
}
class Consumer extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
// The value is no-longer converted to AsyncValue,
// and the created stream is returned as is.
Stream<int> stream = ref.watch(rawStreamProvider);
return StreamBuilder<int>(
stream: stream,
builder: (context, snapshot) {
return Text('${snapshot.data}');
},
);
}
}