Salta al contenuto principale

Websocket ed esecuzione sincrona

Fino ad ora abbiamo solo coperto come creare un Future. Abbiamo fatto ciò apposta, dato che i Futures sono una parte fondamentale di come le applicazioni che usano Riverpod dovrebbero essere costruite. Ma, Riverpod supporta anche altri formati se necessario.

In particolare, invece di un Future, i provider sono liberi di:

  • Ritornare in modo sincrono un oggetto, come creare un "Repository".
  • Ritornare uno Stream, come ascoltare i websocket.

Restituire un Future e restituire uno Stream o un oggetto è abbastanza simile nel complesso. Considera questa pagina come una spiegazione delle sottili differenze e de vari suggerimenti per questi casi d'uso.

Restituire un oggetto in modo sincrono

Per creare un oggetto in modo sincrono, assicurati che il tuo provider non ritorni un Future:


int synchronousExample(Ref ref) {
return 0;
}

Quando un provider crea un oggetto in modo sincrono, questo impatta come quell'oggetto verrà consumato. In particolare, i valori sincroni non sono contenuti in un "AsyncValue":

  Consumer(
builder: (context, ref, child) {
// The value is not wrapped in an "AsyncValue"
int value = ref.watch(synchronousExampleProvider);

return Text('$value');
},
);

La conseguenza di questa differenza è che se il tuo provider lancia un'eccezione, provare a leggere il valore ricauserà l'errore. In alternativa, quando si usa ref.listen, la callback "onError" sarà invocata.

Considerazioni sugli oggetti "Listenable"

Gli oggetti Listenable come ChangeNotifier or StateNotifier non sono supportati. Se, per motivi di compatibilità, è necessario interagire con uno di questi oggetti, un workaround consiste nel collegare il loro meccanismo di notifica a 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;
}
info

Nel caso in cui tu abbia bisogno di questa logica molte volte, è importante notare che la logica è condivisa! L'oggetto "ref" è progettato per essere componibile. Ciò consente di estrarre la logica di dispose/ascolto dal 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));
}

Ascoltare uno Stream

Un caso d'uso comune delle moderne applicazioni è interagire con websockets, come Firbase o GraphQL subscriptions. Interagire con queste API è spesso fatto ascoltando uno Stream.

Per aiutarci in tale scopo, Riverpod supporta di natura gli oggetti Stream. Come con i Future, l'oggetto verrà convertito in un 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(),
};
}
}
info

Riverpod non è consapevole delle implementazioni personalizzate di Stream, come ad esempio BehaviorSubject di RX. Pertanto, restituire un BehaviorSubject non esporrà il valore in modo sincrono ai widget, anche se è già disponibile alla creazione.

Di default, Riverpod convertirà Stream e Future in AsyncValue. Anche se raramente necessario, è possibile disattivare questo comportamento wrappando il tipo del valore di ritorno in un typedef Raw.

caution

È generalmente sconsigliato disabilitare la conversione in AsyncValue. Fallo solo se sai quello che stai facendo.


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