Salta al contenuto principale

Provider

Provider è il provider più basico tra tutti. Crea un valore... e questo è tutto.

Provider è utilizzato tipicamente per:

  • calcoli per memorizzazione in cache
  • esporre un valore ad altri provider (come Repository/HttpClient).
  • offrire un modo per test o widget di sovrascrivere un valore.
  • ridurre il numero di rebuilds dei provider/widget senza dover usare select.

Usare Provider per memorizzare calcoli/computazioni

Provider è un potente strumento per memorizzare operazioni sincrone quando combinato con ref.watch.

Un esempio potrebbe essere filtrare una lista di todo. Dato che filtrare una lista potrebbe risultare leggermente costoso, idealmente non vogliamo filtrare la nostra lista di todo ogni volta che la nostra applicazione si renderizza. In questa situazione possiamo usare Provider per fare filtrare al posto nostro.

Per fare ciò, assumiamo che la nostra applicazione abbia un esistente StateNotifierProvider che manipola una lista di todo:


class Todo {
Todo(this.description, this.isCompleted);
final bool isCompleted;
final String description;
}

class TodosNotifier extends StateNotifier<List<Todo>> {
TodosNotifier() : super([]);

void addTodo(Todo todo) {
state = [...state, todo];
}
// TODO aggiungere altri metodi, come "removeTodo", ...
}

final todosProvider = StateNotifierProvider<TodosNotifier, List<Todo>>((ref) {
return TodosNotifier();
});

Da qui, possiamo usare Provider per esporre la lista dei todo filtrata, mostrando solo i todo completati:


final completedTodosProvider = Provider<List<Todo>>((ref) {
// Otteniamo la lista di tutti i todo da todosProvider
final todos = ref.watch(todosProvider);

// restituiamo solo i todo completati
return todos.where((todo) => todo.isCompleted).toList();
});

Con questo codice, la nostra UI è ora in grado di mostrare la lista dei todo completati stando in ascolto di completedTodosProvider:

Consumer(builder: (context, ref, child) {
final completedTodos = ref.watch(completedTodosProvider);
// TODO mostrare i todo usando ListView/GridView/.../* SKIP */
return Container();
/* SKIP END */
});

La parte interessante è che la lista filtrata ora è memorizzata in cache.

Ciò significa che la lista dei todo completati non sarà ricalcolata fino a quando i todo verranno aggiunti/rimossi/aggiornati, anche se stiamo leggendo la lista dei todo completati più volte.

Tieni presente che non è necessario invalidare manualmente la cache quando la lista dei todo cambia. Provider sà in modo autonomo quando il risultato deve essere ricalcolato grazie a ref.watch.

Ridurre il numero di rebuilds dei provider/widget attraverso Provider

Un aspetto unico di Provider è che anche quando Provider viene ricalcolato (in genere quando si usa ref.watch), non aggiornerà i widget/provider che lo ascoltano a meno che il valore non cambi.

Un esempio reale potrebbe essere per abilitare/disabilitare i tasti previous/next di una vista paginata:

Tasto "Precedente/Successivo"

Nel nostro caso ci concentreremo specificamente sul tasto "Precedente" ("Previous"). Un'implementazione ingenua di tale pulsante sarebbe un widget che ottiene l'indice della pagina corrente, e se quell'indice è uguale a 0, disabiliteremmo il pulsante.

Tale codice potrebbe essere:


final pageIndexProvider = StateProvider<int>((ref) => 0);

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


Widget build(BuildContext context, WidgetRef ref) {
// se non è la prima pagina, il pulsante "previous" è attivo
final canGoToPreviousPage = ref.watch(pageIndexProvider) == 0;

void goToPreviousPage() {
ref.read(pageIndexProvider.notifier).update((state) => state - 1);
}

return ElevatedButton(
onPressed: canGoToPreviousPage ? goToPreviousPage : null,
child: const Text('previous'),
);
}
}

Il problema con questo codice è che ogni volta che cambiamo la pagina corrente, il pulsante "Previous" verrà ricostruito. Come funzionamento ideale, vorremmo che il pulsante si ricostruisse solo quando passa da attivato a disattivato.

La radice del problema è che stiamo ricalcolando se l'utente è autorizzato ad andare alla pagina precedente direttamente all'interno del pulsante "previous".

Un modo per risolvere questo problema è estrarre la logica al di fuori del widget in un Provider:


final pageIndexProvider = StateProvider<int>((ref) => 0);

// Un provider che calcola se l'utente è abilitato ad andare alla pagina precedente

final canGoToPreviousPageProvider = Provider<bool>((ref) {
return ref.watch(pageIndexProvider) == 0;
});

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


Widget build(BuildContext context, WidgetRef ref) {
// Ora osserviamo il nostro nuovo Provider
// Il nostro widget non calcolerà più se possiamo andare alla pagina precedente.
final canGoToPreviousPage = ref.watch(canGoToPreviousPageProvider);

void goToPreviousPage() {
ref.read(pageIndexProvider.notifier).update((state) => state - 1);
}

return ElevatedButton(
onPressed: canGoToPreviousPage ? goToPreviousPage : null,
child: const Text('previous'),
);
}
}

Facendo questa piccola modifica, il nostro widget PreviousButton non verrà più ricostruito
quando l'indice della pagina cambia grazie a Provider.

D'ora in poi, quando l'indice della pagina cambierà, il provider canGoToPreviousPageProvider sarà ricalcolato. Ma se il valore esposto dal provider non cambia, allora PreviousButton non verrà ricostruito.

Questa modifica ha migliorato la performance del nostro bottone, e ha avuto l'interessante beneficio di estrarre la logica al di fuori del nostro widget.