Salta al contenuto principale

StateProvider

StateProvider è un provider che espone un modo per modificare il suo stato. É una semplificazione di StateNotifierProvider e progettato per evitare di dover scrivere una classe StateNotifier per casi d'uso molto semplici.

StateProvider esiste principalmente per consentire la modifica di variabili semplici da parte dell'interfaccia utente. Lo stato di uno StateProvider è generalmente:

  • un enum, come può essere un tipo di filtro
  • una String, tipicamente il contenuto grezzo di un campo di testo
  • un booleano, per le checkbox
  • un numero, per impaginazione e campi d'età

Non dovresti usare StateProvider se:

  • il tuo stato necessita di logica di validazione
  • il tuo stato è un oggetto complesso (come una classe personalizzata, una lista/map ecc)
  • la logica per modificare il tuo stato è più avanzata di un semplice count++.

Per casi più avanzati, considera usare StateNotifierProvider e creare una classe StateNotifier. Anche se il codice boilerplate inizialmente sarà più grande, avere una classe StateNotifier personalizzata è fondamentale per la mantenibilità a lungo termine del tuo progetto - poichè centralizza la logica del tuo stato in un'unica posizione.

Esempio d'uso: cambiare il tipo di filtro usando una dropdown

Un caso d'uso reale di StateProvider sarebbe quello di gestire lo stato di semplici componenti di form come dropdowns/campi di testo/checkboxes.

In particolare, vedremo come usare StateProvider per implementare una dropdown che permetta di cambiare come una lista di prodotti è ordinata.

Per semplificare le cose, la lista dei prodotti che otterremo sarà costruita direttamente nell'applicazione e sarà come di seguito:


class Product {
Product({required this.name, required this.price});

final String name;
final double price;
}

final _products = [
Product(name: 'iPhone', price: 999),
Product(name: 'cookie', price: 2),
Product(name: 'ps5', price: 500),
];

final productsProvider = Provider<List<Product>>((ref) {
return _products;
});

In un'applicazione reale, questa lista sarebbe stata generalmente ottenuta usando FutureProvider facendo una richiesta di rete.

L'interfaccia utente può mostrare quindi la lista dei prodotti scrivendo:


Widget build(BuildContext context, WidgetRef ref) {
final products = ref.watch(productsProvider);
return Scaffold(
body: ListView.builder(
itemCount: products.length,
itemBuilder: (context, index) {
final product = products[index];
return ListTile(
title: Text(product.name),
subtitle: Text('${product.price} \$'),
);
},
),
);
}

Ora che abbiamo finito con la base, possiamo aggiungere una dropdown, la quale ci permetterà di filtrare i nostri prodotti sia per prezzo che per nome. Per questo, useremo DropDownButton.


// Un enum rappresentante il tipo di filtro
enum ProductSortType {
name,
price,
}

Widget build(BuildContext context, WidgetRef ref) {
final products = ref.watch(productsProvider);
return Scaffold(
appBar: AppBar(
title: const Text('Products'),
actions: [
DropdownButton<ProductSortType>(
value: ProductSortType.price,
onChanged: (value) {},
items: const [
DropdownMenuItem(
value: ProductSortType.name,
child: Icon(Icons.sort_by_alpha),
),
DropdownMenuItem(
value: ProductSortType.price,
child: Icon(Icons.sort),
),
],
),
],
),
body: ListView.builder(
// ... /* SKIP */
itemBuilder: (c, i) => Container(), /* SKIP END */
),
);
}

Ora che abbiamo una dropdown, creiamo uno StateProvider e sincronizziamo lo stato della dropdown con il nostro provider.

Per prima cosa, creiamo il provider con StateProvider:


final productSortTypeProvider = StateProvider<ProductSortType>(
// Restituiamo il tipo di ordinamento di default, in questo caso 'name'.
(ref) => ProductSortType.name,
);

Successivamente possiamo connettere questo provider con la nostra dropdown scrivendo:

DropdownButton<ProductSortType>(
// Quando il tipo di ordinamento cambia, ricostruirà la dropdown
// per aggiornare l'icona mostrata.
value: ref.watch(productSortTypeProvider),
// Quando l'utente interagisce con la dropdown aggiorniamo lo stato del provider.
onChanged: (value) => ref.read(productSortTypeProvider.notifier).state = value!,
items: [
// ...
],
),

Con questo, ora dovremmo essere in grado di cambiare il tipo di ordinamento. Tuttavia, non ha ancora alcun impatto sulla lista dei prodotti! É ora della parte finale: aggiornare productsProvider per ordinare la lista dei prodotti.

Una compenente chiave di tale implementazione è di usare ref.watch, per far sì che productsProvider ottenga il tipo di ordinamento e ricalcoli la lista dei prodotti ogni volta che il tipo di ordinamento cambia.

L'implementazione sarà:


final productsProvider = Provider<List<Product>>((ref) {
final sortType = ref.watch(productSortTypeProvider);
switch (sortType) {
case ProductSortType.name:
return _products.sorted((a, b) => a.name.compareTo(b.name));
case ProductSortType.price:
return _products.sorted((a, b) => a.price.compareTo(b.price));
}
});

Questo è quanto! Questa modifica basta per fare in modo che l'interfaccia utente ri-renderizzi automaticamente la lista dei prodotti quando il tipo di ordinamento cambia.

Di seguito l'esempio completo su Dartpad:

Come aggiornare lo stato basandosi sul valore precedente senza leggere il provider due volte

Delle volte, potresti voler aggiornare lo stato di uno StateProvider basandosi sul valore precedente. Naturalmente, potresti finire per scrivere:


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

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


Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () {
// We're updating the state from the previous value, we ended-up reading
// the provider twice!
// Stiamo aggiornando lo stato dal valore precedente, siamo finiti per
// leggere il provider due volte!
ref.read(counterProvider.notifier).state = ref.read(counterProvider.notifier).state + 1;
},
),
);
}
}

Anche se non c'è niente di particolarmente sbagliato in questo codice, la sintassi è un po' scomoda.

Per migliorare la sintassi, possiamo usare la funzione update. Tale funzione prenderà una funzione callback che riceverà lo stato corrente e dovrebbe restituire il nuovo stato. Possiamo usarla per riscrivere il nostro codice precedente in:


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

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


Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () {
ref.read(counterProvider.notifier).update((state) => state + 1);
},
),
);
}
}

Questa modifica ottiene lo stesso effetto ma migliora leggermente la sintassi.