Salta al contenuto principale

Effettua la tua prima richiesta di provider/rete

Le richieste di rete sono il nucleo di qualsiasi applicazione. Tuttavia, ci sono molte cose da considerare quando si effettua una richiesta di rete:

  • L'interfaccia utente dovrebbe visualizzare uno stato di caricamento durante la richiesta
  • Gli errori dovrebbero essere gestiti in modo adeguato
  • La richiesta dovrebbe essere memorizzata nella cache se possibile

In questa sezione, vedremo come Riverpod può aiutarci a gestire tutto ciò in modo naturale.

Impostare ProviderScope

Prima di iniziare a effettuare richieste di rete, assicurati che ProviderScope sia aggiunto alla radice dell'applicazione.

void main() {
runApp(
// Per installare Riverpod dobbiamo aggiungere questo widget al di sopra di tutto.
// Questo widget non dovrebbe essere dentro "MyApp" ma direttamente come parametro di "runApp"
ProviderScope(
child: MyApp(),
),
);
}

Farlo abiliterà Riverpod per l'intera applicazione.

note

Per i passaggi completi di installazione (come l'installazione di riverpod_lint e l'esecuzione del generatore di codice), dai un'occhiata a Introduzione.

Eseguire la tua richiesta di rete in un "provider"

Eseguire una richiesta di rete è generalmente ciò che chiamiamo "business logic". In Riverpod, la business logic è collocata all'interno dei "provider". Un provider è una funzione super potenziata. Si comportano come funzioni normali, con i vantaggi aggiuntivi di:

  • essere cachati
  • offrire una gestione degli errori/caricamenti di default
  • essere ascoltabili
  • eseguirsi automaticamente quando alcuni dati cambiano

Questo rende i provider perfetti per le richieste di rete GET (per le richieste POST/etc, consulta Eseguire side effects).

Come esempio, creiamo una semplice applicazione che suggerisce un'attività casuale da fare quando si è annoiati. Per farlo, utilizzeremo il Bored API. In particolare, effettueremo una richiesta GET all'endpoint /api/activity. Questo restituisce un oggetto JSON, che convertiremo in un'istanza di una classe Dart. Il passo successivo sarebbe quindi quello di visualizzare questa attività nell'interfaccia utente. Ci assicureremo anche di visualizzare uno stato di caricamento durante la richiesta e di gestire gli errori in modo opportuno.

Sembra fantastico? Facciamolo!

Definizione del modello

Prima di iniziare, dobbiamo definire il modello dei dati che riceveremo dall'API. Questo modello avrà anche bisogno di un modo per convertire l'oggetto JSON in un'istanza di classe Dart.

In generale, è consigliato utilizzare un code-generator come Freezed o json_serializable per gestire la decodifica JSON. Ma naturalmente, è anche possibile farlo manualmente.

Ecco il nostro modello:

activity.dart

import 'package:freezed_annotation/freezed_annotation.dart';

part 'activity.freezed.dart';
part 'activity.g.dart';

/// La risposta dell'endpoint `GET /api/activity`.
///
/// È definita utilizzando `freezed` e `json_serializable`..

class Activity with _$Activity {
factory Activity({
required String key,
required String activity,
required String type,
required int participants,
required double price,
}) = _Activity;

/// Converte un oggetto JSON in un'istanza di [Activity].
/// Questo consente una lettura type-safe della risposta API.
factory Activity.fromJson(Map<String, dynamic> json) => _$ActivityFromJson(json);
}

Creazione del provider

Ora che abbiamo il nostro modello, possiamo cominciare ad interrogare l'API. Per fare ciò avremo bisogno di creare il nostro primo provider.

La sintassi per definire un provider è come segue:

@riverpod
Result myFunction(Ref ref) {
  <your logic here>
}
L'annotazione

Tutti i provider devono essere annotati con @riverpod or @Riverpod(). Questa annotazione può essere posizionata su funzioni globali o classi. Mediante quest'annotazione è possibile configurare il provider.

Per esempio, possiamo disabilitare "auto-dispose" (che vedremo più tardi) scrivendo @Riverpod(keepAlive: true).

La funzione annotataIl nome della funzione annotata determina come verrà interagito con il provider. Data una funzione `myFunction`, una variabile `myFunctionProvider` verrà generata.

Le funzioni annotate devono specificare "ref" come primo parametro. Oltre a ciò, la funzione può avere un qualsiasi numero di parametri, compresi i generici. La funzione è libera di restituire un Future/Stream se lo desidera.

Questa funzione sarà chiamata quando il provider viene letto la prima volta. Le letture successive non chiameranno nuovamente la funzione, ma restituiranno invece il valore memorizzato nella cache.

Ref

Un oggetto usato per interagire con gli altri provider. Tutti i provider ne hanno uno; o come parametro della funzione del provider, o come proprietà di un Notifier. Il tipo di questo oggetto è determinato dal nome della funzione/classe.

Nel nostro caso vogliamo ottenere (GET) un attività dall'API. Dato che una GET è un'operazione sincrona vorremo creare un Future<Activity>.

Usando la sintassi descritta precedentemente, possiamo quindi definire il nostro provider come segue:

provider.dart

import 'dart:convert';

import 'package:http/http.dart' as http;
import 'package:riverpod_annotation/riverpod_annotation.dart';

import 'activity.dart';

// Necessario affinché la generazione del codice funzioni
part 'provider.g.dart';

/// This will create a provider named `activityProvider`
/// which will cache the result of this function.

Future<Activity> activity(Ref ref) async {
// Usando il package http, otteniamo un'attività casuale dalle Bored API
final response = await http.get(Uri.https('boredapi.com', '/api/activity'));
// Usando dart:convert, decodifichiamo il payload JSON in una Map.
final json = jsonDecode(response.body) as Map<String, dynamic>;
// Infine, convertiamo la mappa in un'istanza Activity
return Activity.fromJson(json);
}

In questo snippet, abbiamo definito un provider chiamato activityProvider che potrà essere usato dalla nostra UI per ottenere un'attività casuale. Vale la pena notare che:

  • La richiesta di rete non verrà eseguita fino a quando l'UI non leggerà il provider almeno una volta.
  • Le letture successive non ri-eseguiranno la richiesta di rete, ma restituiranno invece l'attività precedentemente recuperata.
  • Se l'UI smette di utilizzare questo provider, la cache verrà distrutta. Quindi, se l'UI utilizza nuovamente il provider, verrà effettuata una nuova richiesta di rete.
  • Non abbiamo gestito gli errori. Questo è intenzionale, poiché i provider gestiscono nativamente gli errori. Se la richiesta di rete o il parsing JSON solleva un'eccezione, l'errore verrà catturato da Riverpod. Quindi, l'interfaccia utente avrà automaticamente le informazioni necessarie per visualizzare una pagina di errore.
info

I provider sono "pigri" (lazy). Definire un provider non eseguirà la richiesta di rete. Invece, la richiesta verrà eseguita alla prima lettura del provider.

Visualizzare la risposta della richiesta di rete nell'UI

Ora che abbiamo definito un provider possiamo iniziare ad utilizzarlo dentro la nostra interfaccia utente per mostrare l'attività.

Per interagire con un provider abbiamo bisogno di un oggetto chiamato "ref". Potresti averlo visto precedentemente nella definizione del provider, dato che i provider posseggono di natura l'accesso all'oggetto "ref". Nel nostro caso però non siamo in un provider, ma in un widget. Quindi come possiamo ottenere un "ref"?

La soluzione è utilizzare un widget personalizzato chiamato Consumer. Un Consumer è un widget simile a Builder ma con il beneficio aggiunto di offrirci un oggetto "ref". Ciò permette alla nostra UI di leggere i provider. L'esempio seguente mostra come usare un Consumer.

consumer.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

import 'activity.dart';
import 'provider.dart';

/// La homepage della nostra applicazione
class Home extends StatelessWidget {
const Home({super.key});


Widget build(BuildContext context) {
return Consumer(
builder: (context, ref, child) {
// Legge l'activityProvider. Questo avvierà la richiesta di rete
// se non era già in corso.
// Utilizzando ref.watch, questo widget si ricostruirà ogni volta che
// activityProvider si aggiorna. Ciò può verificarsi quando:
// - La risposta passa da "loading" a "data/error"
// - La richiesta è stata aggiornata
// - Il risultato è stato modificato localmente (ad esempio durante l'esecuzione di side-effects)
// ...
final AsyncValue<Activity> activity = ref.watch(activityProvider);

return Center(
/// Poiché le richieste di rete sono asincrone e possono fallire, è necessario
/// gestire sia gli stati di errore che di caricamento.
/// Possiamo utilizzare il pattern matching per fare ciò.
/// In alternativa, potremmo utilizzare `if (activity.isLoading) { ... } else if (...)`
child: switch (activity) {
AsyncData(:final value) => Text('Activity: ${value.activity}'),
AsyncError() => const Text('Oops, something unexpected happened'),
_ => const CircularProgressIndicator(),
},
);
},
);
}
}

In questo snippet abbiamo usato un Consumer per leggere il nostro activityProvider e visualizzare l'attività. Abbiamo anche gestito in modo elegante gli stati di caricamento/errore. Si noti come l'UI è stata in grado di gestire gli stati di caricamento/errore senza dover fare qualcosa di speciale nel provider. Allo stesso tempo, se il widget dovesse ricostruirsi, la richiesta di rete non verrebbe correttamente eseguita nuovamente. Anche altri widget potrebbero accedere allo stesso provider senza rieseguire la richiesta di rete.

info

I widget possono ascoltare quanti provider vogliono. Per fare ciò, basta aggiungere più chiamate ref.watch.

tip

Assicurati di installare il linter. Permetterà al tuo IDE di fornirti opzioni di refactoring per aggiungere automaticamente un Consumer o convertire uno StatelessWidget in un ConsumerWidget.

Vedere Introduzione per i passaggi di installazione.

Andando oltre: Rimuovere l'indentazione usando ConsumerWidget invece di Consumer.

Nell'esempio precedente abbiamo usato un Consumer per leggere il nostro provider. Nonostante non ci sia nulla di sbagliato in questo approccio, l'indentazione aggiunta può rendere più difficile la lettura del codice.

Riverpod offre un modo alternativo per ottenere lo stesso risultato: Invece di scrivere uno StatelessWidget/StatefulWidget che ritorna un Consumer, possiamo definire un ConsumerWidget/ConsumerStatefulWidget. ConsumerWidget e ConsumerStatefulWidget sono in pratica la fusione di uno StatelessWidget/StatefulWidget e di un Consumer. Si comportano allo stesso modo delle loro controparti originali ma con il beneficio aggiunto di offrire un "ref".

Possiamo riscrivere gli esempi precedenti per usare ConsumerWidget come segue:


/// Estendiamo "ConsumerWidget" al posto di "StatelessWidget".
/// Ciò è equivalente a creare uno "StatelessWidget" e ritornare un widget "Consumer".
class Home extends ConsumerWidget {
const Home({super.key});


// Si noti come il metodo "build" ora riceve un extra parametro: "ref"
Widget build(BuildContext context, WidgetRef ref) {
// Possiamo usare "ref.watch" dentro il nostro widget come facevamo con "Consumer"
final AsyncValue<Activity> activity = ref.watch(activityProvider);

// Il codice dell'interfaccia grafica rimane la stesso
return Center(/* ... */);
}
}

Per quanto riguarda ConsumerStatefulWidget scriveremo invece:


// Estendiamo "ConsumerStatefulWidget".
// Questo è l'equivalente di usare "Consumer" in un "StatefulWidget"
class Home extends ConsumerStatefulWidget {
const Home({super.key});


ConsumerState<ConsumerStatefulWidget> createState() => _HomeState();
}

// Si noti come invece di "State", stiamo estendendo "ConsumerState".
// Ciò usa lo stesso principio di "ConsumerWidget" al posto di "StatelessWidget"
class _HomeState extends ConsumerState<Home> {

void initState() {
super.initState();

// Anche i cicli di vita dello stato hanno accesso a "ref".
// Ciò consente operazioni come l'aggiunta di un listener a un provider specifico
// per mostrare dialoghi/snackbars.
ref.listenManual(activityProvider, (previous, next) {
// TODO mostrare una snackbar/dialog
});
}


Widget build(BuildContext context) {
// "ref" non è più passato come parametro, ma è invece una proprietà di "ConsumerState".
// Possiamo quindi tenere "ref.watch" dentro il metodo "build".
final AsyncValue<Activity> activity = ref.watch(activityProvider);

return Center(/* ... */);
}
}

Considerazioni su flutter_hooks: combinare HookWidget e ConsumerWidget

caution

Se non hai mai sentito parlare di "hooks" prima sentiti libero di saltare questa sezione. Flutter_hooks è un package indipendente da Riverpod ma spesso usato insieme. Se sei nuovo a Riverpod, l'uso degli "hooks" è sconsigliato. Vedi di più in Informazioni sugli hook.

Se stai usando flutter_hooks, ti starai chiedendo come combinare HookWidget e ConsumerWidget. Dopo tutto, entrambi comportano modificare la classe widget estesa.

Riverpod offre una soluzione per questo problema: HookConsumerWidget e StatefulHookConsumerWidget. In modo simile a come ConsumerWidget e ConsumerStatefulWidget sono la fusione di Consumer e StatelessWidget/StatefulWidget, HookConsumerWidget and StatefulHookConsumerWidget sono la fusione di Consumer e HookWidget/HookStatefulWidget. Come tali, permettono di usare sia gli hooks che i provider nello stesso widget.

Per illustrarlo, potremmo riscrivere ancora una volta l'esempio precedente:


/// Estendiamo "HookConsumerWidget".
/// Questo combina "StatelessWidget" + "Consumer" + "HookWidget" insieme.
class Home extends HookConsumerWidget {
const Home({super.key});


// Si noti come il metodo "build" ora riceve un extra parametro: "ref"
Widget build(BuildContext context, WidgetRef ref) {
// È possibile usare gli hooks come "useState" all'interno del widget
final counter = useState(0);

// Possiamo anche leggere provider
final AsyncValue<Activity> activity = ref.watch(activityProvider);

return Center(/* ... */);
}
}