Aller au contenu principal

StateProvider

StateProvider est un provider qui expose un moyen de modifier son état. C'est une simplification de StateNotifierProvider, conçue pour éviter d'avoir à écrire une classe StateNotifier pour des cas d'utilisation très simples.

StateProvider existe principalement pour permettre la modification de variables simples par l'interface utilisateur.
L'état d'un StateProvider est typiquement un :

  • un enum, tel qu'un type de filtre
  • un String, généralement le contenu brut d'un champ de texte.
  • un boolean, pour les checkboxes
  • un nombre, pour la pagination ou champs du formulaire d'âge

Vous ne devez pas utiliser StateProvider si :

  • votre état a besoin d'une logique de validation
  • votre état est un objet complexe (comme une classe personnalisée, une liste ou une carte, etc.)
  • la logique de modification de votre état est plus avancée qu'un simple count++.

Pour les cas plus avancés, envisagez d'utiliser StateNotifierProvider à la place et créez une classe StateNotifier.
Bien que le modèle initial soit un peu plus volumineux, le fait d'avoir une classe StateNotifier personnalisée est essentiel pour la maintenabilité à long terme de votre application, car elle centralise le business logic de votre état en un seul endroit.

Exemple d'utilisation : Changer le type de filtre à l'aide d'une liste déroulante

Un cas concret d'utilisation de StateProvider serait de gérer l'état de composants de formulaires simples, comme les dropdown, les champs de texte et les checkboxes. En particulier, nous verrons comment utiliser StateProvider pour implémenter un dropdown qui permet de changer la façon dont une liste de produits est triée.

Pour des raisons de simplicité, la liste des produits que nous obtiendrons sera construite directement dans l'application et sera comme suit :


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;
});

Dans une application réelle, cette liste est généralement obtenue à l'aide des éléments suivants FutureProvider en faisant une requête au réseau.

L'interface utilisateur pourrait alors afficher la liste des produits en faisant :


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} \$'),
);
},
),
);
}

Maintenant que nous en avons terminé avec la base, nous pouvons ajouter un dropdown, qui permettra de filtrer nos produits soit par prix, soit par nom.
Pour cela, on utilisera DropDownButton.


// Un enum représentant le type de filtre
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 */
),
);
}

Maintenant que nous avons un dropdown, créons un StateProvider et synchronisons l'état de la liste déroulante avec notre provider.

tout d'abord, on crée notre StateProvider:


final productSortTypeProvider = StateProvider<ProductSortType>(
// On retourne le type de tri par défaut, ici nom.
(ref) => ProductSortType.name,
);

Ensuite, on peut connecter ce provider avec notre dropdown en faisant :

  return AppBar(actions: [
DropdownButton<ProductSortType>(
// Lorsque le type de tri change, le dropdown est
// reconstruite pour mettre à jour l'icône affichée.
value: ref.watch(productSortTypeProvider),
// Lorsque l'utilisateur interagit avec le dropdown,
// on met à jour l'état (state) du provider.
onChanged: (value) =>
ref.read(productSortTypeProvider.notifier).state = value!,
items: [
// ...
],
),

Avec cela, nous devrions maintenant être en mesure de changer le type de tri.
Mais cela n'a pas d'impact sur la liste des produits ! C'est maintenant l'heure de la dernière partie : Mettre à jour notre productsProvider pour trier la liste des produits.

Un élément clé de l'implémentation est l'utilisation de ref.watch, pour que notre productsProvider d'obtenir le type de tri et de recalculer la liste des produits chaque fois que le type de tri change.

La mise en œuvre serait :


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));
}
});

C'est tout ! Cette modification est suffisante pour que l'interface utilisateur puisse automatiquement re-affiche la liste des produits lorsque le type de tri change.

Voici l'exemple complet sur Dartpad :

Comment mettre à jour l'état en fonction de la valeur précédente sans lire le provider deux fois

Parfois, vous voulez mettre à jour l'état d'un StateProvider en fonction de la valeur précédente. Naturellement, vous pouvez finir par écrire :


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: () {
// On et à jour l'état à partir de la valeur précédente,
// on fini par lire le provider deux fois !
ref.read(counterProvider.notifier).state = ref.read(counterProvider.notifier).state + 1;
},
),
);
}
}

Bien qu'il n'y ait rien de particulièrement mauvais dans cet extrait, la syntaxe est un peu incommode.

Pour améliorer un peu la syntaxe, on peut utiliser la fonction update. Cette fonction prendra un callback qui recevra l'état actuel et est censé retourner le nouvel état. On peut l'utiliser pour refactoriser notre code précédent en :


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);
},
),
);
}
}

Cette modification permet d'obtenir le même effet tout en améliorant un peu la syntaxe.