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(SynchronousExampleRef 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) {
// Il valore non è contenuto in un "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(MyListenableRef ref) {
final notifier = ValueNotifier(0);

// Smaltiamo il notifier quando il provider viene distrutto
ref.onDispose(notifier.dispose);

// Notifica i listener di questo provider ogni volta che il ValueNotifier si aggiorna.
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 {
// Possiamo spostare la logica precedente in una estensione Ref.
// Questo abilita il riutilizzo della logica
T disposeAndListenChangeNotifier<T extends ChangeNotifier>(T notifier) {
onDispose(notifier.dispose);
notifier.addListener(notifyListeners);
// Restituiamo il notifier per facilitarne di un poco l'utilizzo
return notifier;
}
}


ValueNotifier<int> myListenable(MyListenableRef ref) {
return ref.disposeAndListenChangeNotifier(ValueNotifier(0));
}


ValueNotifier<int> anotherListenable(AnotherListenableRef 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(StreamExampleRef ref) async* {
// Ogni secondo ritorna un numero da 0 a 41.
// Questo può essere sostituito con uno Stream da Firestore o GraphQL o altro.
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) {
// Lo stream è ascoltato e convertito in un AsyncValue
AsyncValue<int> value = ref.watch(streamExampleProvider);

// Possiamo usare l'AsyncValue per gestire i stati di caricamento/errore e mostrare il dato.
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(RawStreamRef ref) {
// "Raw" è un typedef. Non c'è bisogno di wrappare
// il valore di ritorno in un costruttore "Raw".
return const Stream<int>.empty();
}

class Consumer extends ConsumerWidget {

Widget build(BuildContext context, WidgetRef ref) {
// Il valore non è più convertito in AsyncValue
// e lo stream creato è ritornato come tale.
Stream<int> stream = ref.watch(rawStreamProvider);
return StreamBuilder<int>(
stream: stream,
builder: (context, snapshot) {
return Text('${snapshot.data}');
},
);
}
}