Skip to main content

Websockets and synchronous execution

So far, we've only covered on how to create a Future.
This is on purpose, as Futures 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(SynchronousExampleRef 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(MyListenableRef 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;
}
info

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(MyListenableRef ref) {
return ref.disposeAndListenChangeNotifier(ValueNotifier(0));
}


ValueNotifier<int> anotherListenable(AnotherListenableRef 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 Futures, the object will be converted to an AsyncValue:


Stream<int> streamExample(StreamExampleRef 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(),
};
}
}
info

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 Streams/Futures to AsyncValue

By default, Riverpod will convert Streams and Futures to AsyncValue. Although rarely needed, it is possible to disable this behavior by wrapping the return type in a Raw typedef.

caution

It is generally discouraged to disable the AsyncValue conversion. Do so only if you know what you are doing.


Raw<Stream<int>> rawStream(RawStreamRef 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}');
},
);
}
}