Salta al contenuto principale

Leggere un provider

Prima di leggere questa guida, assicurati di aver letto prima Providers.

In questa guida vedremo come consumare un provider.

Ottenere un oggetto "ref"

Innanzitutto, prima di leggere un provider abbiamo bisogno di ottenere un oggetto "ref".

Questo oggetto ci permette di interagire con i provider, che si tratti di un widget o di un altro provider.

Ottenere un "ref" da un provider

Tutti i provider ricevono "ref" come parametro:

final provider = Provider((ref) {
// usa ref per ottenere altri provider
final repository = ref.watch(repositoryProvider);

return SomeValue(repository);
})

É sicuro passare questo parametro al valore esposto dal provider.

Per esempio, un uso comune è passare il "ref" del provider ad un StateNotifier:


final counterProvider = StateNotifierProvider<Counter, int>((ref) {
return Counter(ref);
});

class Counter extends StateNotifier<int> {
Counter(this.ref) : super(0);

final Ref ref;

void increment() {
// Counter può usare "ref" per leggere altri provider
final repository = ref.read(repositoryProvider);
repository.post('...');
}
}

Ciò consente alla nostra classe Counter di leggere i provider.

Ottenere un "ref" da un widget

I widget per natura non hanno un parametro ref. Ma Riverpod offre diverse soluzioni per ottenerne uno dai widget.

Estendendo ConsumerWidget al posto di StatelessWidget

La soluzione più comune è di sostituire StatelessWidget con ConsumerWidget quando si crea un widget.

ConsumerWidget è identico a StatelessWidget nell'uso, con la sola differenza che ha un parametro extra nel suo metodo "build": l'oggetto "ref".

Un tipico ConsumerWidget è simile al seguente:


class HomeView extends ConsumerWidget {
const HomeView({super.key});


Widget build(BuildContext context, WidgetRef ref) {
// usa ref per stare in ascolto di un provider
final counter = ref.watch(counterProvider);
return Text('$counter');
}
}

Estendendo ConsumerStatefulWidget+ConsumerState al posto di StatefulWidget+State

Come ConsumerWidget, ConsumerStatefulWidget e ConsumerState sono l'equivalente di uno StatefulWidget con il suo State, con la differenza che lo stato ha un oggetto "ref".

Questa volta, "ref" non è passato come parametro del metodo "build" ma è invece una proprietà dell'oggetto ConsumerState:


class HomeView extends ConsumerStatefulWidget {
const HomeView({super.key});


HomeViewState createState() => HomeViewState();
}

class HomeViewState extends ConsumerState<HomeView> {

void initState() {
super.initState();
// "ref" può essere usato in tutti i cicli di vita di uno StatefulWidget.
ref.read(counterProvider);
}


Widget build(BuildContext context) {
// Possiamo anche usare "ref" per ascoltare un provider all'interno del
// metodo build.
final counter = ref.watch(counterProvider);
return Text('$counter');
}
}

Estendendo HookConsumerWidget al posto di HookWidget

Quest'opzione è specifica per chi usa flutter_hooks. Dato che flutter_hooks richiede l'estensione di HookWidget per funzionare, i widget che usano gli hooks non possono estendere ConsumerWidget.

Il pacchetto hooks_riverpod espone un nuovo widget chiamato HookConsumerWidget. HookConsumerWidget si comporta come un ConsumerWidget e un HookWidget. Ciò consente ad un widget: sia di stare in ascolto dei provider e sia di usare gli hooks.

Un esempio potrebbe essere:


class HomeView extends HookConsumerWidget {
const HomeView({super.key});


Widget build(BuildContext context, WidgetRef ref) {
// HookConsumerWidget consente l'uso degli hooks all'interno
// del metodo build.
final state = useState(0);

// Possiamo anche usare il parametro ref per ascoltare i provider.
final counter = ref.watch(counterProvider);
return Text('$counter');
}
}

Estendendo StatefulHookConsumerWidget invece di HookWidget

Quest'opzione è specifica per chi usa flutter_hooks, per poter usare i metodi del ciclo di vita di uno StatefulWidget in aggiunta agli hooks.

Di seguito un esempio:


class HomeView extends StatefulHookConsumerWidget {
const HomeView({super.key});


HomeViewState createState() => HomeViewState();
}

class HomeViewState extends ConsumerState<HomeView> {

void initState() {
super.initState();
// "ref" può essere usato in tutti i cicli di vita di uno StatefulWidget.
ref.read(counterProvider);
}


Widget build(BuildContext context) {
// Come HookConsumerWidget, possiamo usare gli hooks
// all'interno di builder
final state = useState(0);

// Possiamo anche usare "ref" per ascoltare un provider
// all'interno del metodo build.

final counter = ref.watch(counterProvider);
return Text('$counter');
}
}

Widgets Consumer e HookConsumer

Un ultimo modo per ottenere un "ref" dentro i widget è fare affidamento a Consumer/HookConsumer.

Queste classi sono widget che possono essere usati per ottenere un "ref", con le stesse proprietà di ConsumerWidget/HookConsumerWidget.

In quanto tali, questi widget sono un modo per ottenere un "ref" senza dover definire una classe. Di seguito un esempio:

Scaffold(
body: HookConsumer(
builder: (context, ref, child) {
// Come HookConsumerWidget, possiamo usare gli hooks
// all'interno di builder
final state = useState(0);

// Possiamo anche usare il parametro "ref" per ascoltare i provider.
final counter = ref.watch(counterProvider);
return Text('$counter');
},
),
);

Usare ref per interagire con i provider

Ora che abbiamo un "ref", possiamo iniziare ad usarlo.

Questi sono i tre usi primari di "ref":

  • ottenere il valore di un provider e ascoltarne i cambiamenti, in modo tale che quando questo valore cambia, questo ricostruirà il widget o il provider che ha sottoscritto il valore. Ciò può essere fatto usando ref.watch
  • aggiungendo un "listener" ad un provider, per eseguire un'azione ogni tal volta che il provider cambia, ad esempio aprire una nuova pagina o mostrare una modale. Ciò può essere fatto usando ref.listen.
  • ottenere il valore di un provider ignorandone i cambiamenti. Questo è utile quando abbiamo bisogno del valore di un provider in un evento come "on click". Ciò può essere fatto usando ref.read.
NOTA

Quando possibile, preferire l'uso di ref.watch rispetto a ref.read o ref.listen per implementare una feature. Affidandosi a ref.watch, la tua applicazione diventa sia reattiva che dichiarativa, il che la rende più mantenibile.

Usare ref.watch per osservare un provider

ref.watch è usato all'interno del metodo build di un widget oppure all'interno del body di un provider per fare in modo che il widget/provider ascolti un provider:

Per esempio, un provider può usare ref.watch per combinare provider multipli in un nuovo valore.

Un esempio potrebbe essere filtrare una todo-list. Potremmo avere due provider:

  • filterTypeProvider, un provider che espone il tipo corrente del filtro (nessuno, mostra solo i task completati, ...)
  • todosProvider, un provider che espone l'intera lista dei task

E utilizzando ref.watch, potremmo fare un terzo provider che combina entrambi i precedenti per creare una lista filtrata di task.


final filterTypeProvider = StateProvider<FilterType>((ref) => FilterType.none);
final todosProvider =
StateNotifierProvider<TodoList, List<Todo>>((ref) => TodoList());

final filteredTodoListProvider = Provider((ref) {
// ottiene sia il filtro che la lista dei todo
final FilterType filter = ref.watch(filterTypeProvider);
final List<Todo> todos = ref.watch(todosProvider);

switch (filter) {
case FilterType.completed:
// restituisce la lista dei todo completati
return todos.where((todo) => todo.isCompleted).toList();
case FilterType.none:
// restituisce la lista dei todo non filtrata
return todos;
}
});

Con questo codice, filteredTodoListProvider ora espone la lista dei task filtrata.

La lista filtrata inoltre si aggiornerà automaticamente se, o il filtro o la lista dei task cambierà. Allo stesso tempo, la lista filtrata non verrà ricalcolata se nè il filtro nè la lista dei task verranno modificati.

In maniera simile, un widget potrà usare ref.watch per mostrare il contenuto da un provider e aggiornare l'interfaccia grafica ogni volta che il contenuto cambia:


final counterProvider = StateProvider((ref) => 0);

class HomeView extends ConsumerWidget {
const HomeView({super.key});


Widget build(BuildContext context, WidgetRef ref) {
// usa ref per ascoltare un provider
final counter = ref.watch(counterProvider);

return Text('$counter');
}
}

Questo snippet mostra un widget che ascolta un provider che memorizza un count. Se count cambia, il widget si ricostruirà e la UI si aggiornerà per mostrare il nuovo valore.

ATTENZIONE

Il metodo watch non dovrebbe essere chiamato in modo asincrono, come all'interno di onPressed di un ElevatedButton. E neppure all'interno di initState e altri cicli di vita di State.

In questi casi, considera invece l'utilizzo di ref.read.

Usare ref.listen per reagire al cambiamento di un provider

In maniera simile a ref.watch, è possibile usare ref.listen per osservare un provider.

La principale differenza tra i due è che, invece di ricostruire il widget/provider se il provider ascoltato cambia, usare ref.listen chiamerà una funzione personalizzata.

Ciò può essere utile per eseguire azioni quando un certo cambiamento si verifica, come mostrare una snackbar quando capita un errore.

Il metodo ref.listen necessita di 2 argomenti posizionali, il primo è il Provider e il secondo è la funzione di callback che vogliamo eseguire quando lo stato cambia. Alla funzione di callback, quando viene chiamata, verranno passati due valori: il valore precedente e il nuovo valore dello Stato.

Il metodo ref.listen si può utilizzare all'interno del body di un provider:


final counterProvider = StateNotifierProvider<Counter, int>(Counter.new);

final anotherProvider = Provider((ref) {
ref.listen<int>(counterProvider, (int? previousCount, int newCount) {
print('The counter changed $newCount');
});
// ...
});

o all'interno del metodo build di un widget:


final counterProvider = StateNotifierProvider<Counter, int>(Counter.new);

class HomeView extends ConsumerWidget {
const HomeView({super.key});


Widget build(BuildContext context, WidgetRef ref) {
ref.listen<int>(counterProvider, (int? previousCount, int newCount) {
print('The counter changed $newCount');
});

return Container();
}
}
ATTENZIONE

Il metodo listen non dovrebbe essere chiamato in modo asincrono, come all'interno di onPressed di un ElevatedButton. E neppure all'interno di initState e altri cicli di vita di State.

Usare ref.read per ottenere lo stato di un provider

Il metodo ref.read è un modo per ottenere lo stato di un provider senza starne in ascolto.

Si usa comunemente all'interno di funzioni innescate dalle interazioni dell'utente. Per esempio, possiamo usare ref.read per incremementare un contatore quando l'utente clicca un bottone.


final counterProvider =
StateNotifierProvider<Counter, int>(Counter.new);

class HomeView extends ConsumerWidget {
const HomeView({super.key});


Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () {
// Call `increment()` on the `Counter` class
// Chiama `increment()` della classe `Counter`
ref.read(counterProvider.notifier).increment();
},
),
);
}
}
NOTA

L'uso di ref.read dovrebbe essere evitato il più possibile perchè non è reattivo.

read esiste per casi dove usare watch o listen potrebbe generare errori. Se puoi, è quasi sempre meglio usare watch/listen, specialmente watch.

NON usare ref.read dentro il metodo build

Potresti essere tentato di usare ref.read per ottimizzare le performance di un widget facendo:


final counterProvider = StateProvider((ref) => 0);

Widget build(BuildContext context, WidgetRef ref) {
// usa "read" per ignorare gli aggiornamenti di un provider
final counter = ref.read(counterProvider.notifier);
return ElevatedButton(
onPressed: () => counter.state++,
child: const Text('button'),
);
}

Ma questa è una cattiva pratica e può causare bug che sono difficili da tracciare.

L'uso di ref.read in questa forma è comunemente associato al pensiero "Il valore esposto da un provider non cambia mai quindi usare 'ref.read' è sicuro". Il problema con questa ipotesi è che, mentre oggi quel provider potrebbe effettivamente non aggiornare mai il suo valore, non vi è alcuna garanzia che domani sarà lo stesso.

Il software tende a cambiare molto ed è probabile che nel futuro, un valore che precedentemente non cambiava mai dovrà cambiare. Se si usa ref.read, quando il valore necessiterà di cambiare, si dovrà andare in tutto il codice per modificare ref.read in ref.watch, il che è un approccio soggetto a errori ed è probabile che dimenticherai alcuni casi.

Mentre se usi ref.watch dal principio, avrai meno problemi durante il refactoring.

Ma voglio usare ref.read per ridurre il numero di rebuilds del mio widget

Sebbene l'obiettivo sia lodevole, è importante far notare che puoi ottenere lo stesso effetto (ridurre il numero di builds) usando sempre ref.watch.

I provider offrono diversi modi di ottenere un valore riducendo il numero di rebuilds.

Per esempio, invece di


final counterProvider = StateProvider((ref) => 0);

Widget build(BuildContext context, WidgetRef ref) {
StateController<int> counter = ref.read(counterProvider.notifier);
return ElevatedButton(
onPressed: () => counter.state++,
child: const Text('button'),
);
}

possiamo scrivere:


final counterProvider = StateProvider((ref) => 0);

Widget build(BuildContext context, WidgetRef ref) {
StateController<int> counter = ref.watch(counterProvider.notifier);
return ElevatedButton(
onPressed: () => counter.state++,
child: const Text('button'),
);
}

Entrambi i pezzi di codice compiono lo stesso effetto: il nostro bottone non si ricostruirà quando il contatore incrementa.

D'altra parte, il secondo approccio supporta casi dove il contatore è resettato. Per esempio, un'altra parte dell'applicazione potrebbe chiamare:

ref.refresh(counterProvider);

che ricreerebbe l'oggeto StateController.

Se usassimo ref.read qui, il nostro bottone userebbe l'istanza precedente di StateController, che è stata eliminata e non dovrebbe essere più utilizzata. Usando ref.watch invece, ricostruiremmo correttamente il pulsante per utilizzare il nuovo StateController.

Decidere cosa leggere

In base al provider che si vuole ascoltare, si possono avere più valori possibili che puoi ascoltare.

Come esempio, considera il seguente StreamProvider:

final userProvider = StreamProvider<User>(...);

Leggendo questo userProvider, puoi:

  • leggere in modo sincrono lo stato corrente ascoltando userProvider stesso:

    Widget build(BuildContext context, WidgetRef ref) {
    AsyncValue<User> user = ref.watch(userProvider);

    return user.when(
    loading: () => const CircularProgressIndicator(),
    error: (error, stack) => const Text('Oops'),
    data: (user) => Text(user.name),
    );
    }
  • ottenere lo Stream associato, ascoltando userProvider.stream:

    Widget build(BuildContext context, WidgetRef ref) {
    Stream<User> user = ref.watch(userProvider.stream);
    }
  • ottenere un Future che si risolve all'ultimo valore emesso, ascoltando userProvider.future:

    Widget build(BuildContext context, WidgetRef ref) {
    Future<User> user = ref.watch(userProvider.future);
    }

Altri provider offrono diversi valori alternativi. Per più informazioni, riferirsi alla documentazione di ogni provider consultando riferimento API.

Usare "select" per filtrare le rebuilds

Una caratteristica finale da menzionare legata alla lettura dei provider è l'abilità di ridurre il numero di volte che un widget/provider effettua una rebuild da ref.watch, o come ref.listen esegue una funzione.

Questo è importante da tenere a mente poiché, per default, l'ascolto di un provider agisce sull'intero stato dell'oggetto. Qualche volta però, ad un widget/provider potrebbero interessare solo le modifiche di alcune proprietà invece che dell'intero oggetto.

Per esempio, un provider può esporre User:

abstract class User {
String get name;
int get age;
}

Ma un widget potrebbe usare solo la proprietà name:

Widget build(BuildContext context, WidgetRef ref) {
User user = ref.watch(userProvider);
return Text(user.name);
}

Se ingenuamente abbiamo usato ref.watch, allora il widget verrebbe ricostruito anche quando la proprietà age cambia.

La soluzione a ciò è usare select per dire esplicitamente a Riverpod che vogliamo ascoltare solo le modifiche della proprietà name di User.

Il codice aggiornato sarebbe:

Widget build(BuildContext context, WidgetRef ref) {
String name = ref.watch(userProvider.select((user) => user.name))
return Text(name);
}

Usando select siamo in grando di specificare una funzione che restituisce la proprietà che ci interessa.

Ogni qualvolta che User cambia, Riverpod chiamerà questa funzione e comparerà il risultato precedente e il nuovo. Se sono differenti, Riverpod ricostruirà il widget. Altrimenti, se sono uguali, Riverpod non ricostruirà il widget.

info

É anche possibile usare select con ref.listen:

ref.listen<String>(
userProvider.select((user) => user.name),
(String? previousName, String newName) {
print('The user name changed $newName');
}
);

Così facendo, Riverpod chiamerà il listener solo quando name cambia.

tip

Non devi per forza restituire solo la proprietà dell'oggetto. Qualsiasi valore che sovrascrive == funzionerà Per esempio potresti fare:

final label = ref.watch(userProvider.select((user) => 'Mr ${user.name}'));