Aller au contenu principal

Provider

Provider est le plus basique de tous les providers. Il crée une valeur... Et c'est à peu près tout.

Provider est typiquement utilisé pour :

  • les calculs du caching (mise en cache)
  • exposer une valeur à d'autres providers (comme un Repository/HttpClient).
  • offre un moyen pour les tests ou les widgets de remplacer une valeur.
  • réduire les reconstructions de providers/widgets sans avoir à utiliser select.

Utilisation de Provider pour mettre en cache des calculs

Provider est un outil puissant pour caching (mettre en cache) des opérations synchrones lorsqu'il est combiné avec ref.watch.

Un exemple serait de filtrer une liste de tâches à accomplir. Comme le filtrage d'une liste peut être légèrement coûteux, nous ne voulons idéalement pas filtrer notre liste de tâches à chaque fois que notre application se re-affiche à nouveau (re-render). Dans cette situation, on peut utiliser Provider pour faire le filtrage à notre place.

Pour cela, supposons que notre application possède un StateNotifierProvider existant. qui manipule une liste de tâches :


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 ajouter d'autres méthodes, telles que "removeTodo", ...
}

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

A partir de là, on peut utiliser Provider pour exposer la liste filtrée des todos, en montrant seulement les tâches terminées :


final completedTodosProvider = Provider<List<Todo>>((ref) {
// On obtient la liste de tous les todos à partir du todosProvider.
final todos = ref.watch(todosProvider);

// on retourne seulement les todos complétés
return todos.where((todo) => todo.isCompleted).toList();
});

Avec ce code, notre interface est maintenant capable d'afficher la liste des todos complétés en écoutant le completedTodosProvider :

  return Consumer(builder: (context, ref, child) {
final completedTodos = ref.watch(completedTodosProvider);
// TODO afficher les todos en utilisant un ListView/GridView/... /* SKIP */
return Container();
/* SKIP END */
});

La partie intéressante est que le filtrage de la liste est maintenant mis en cache.

Cela signifie que la liste des tâches terminées ne sera pas recalculée jusqu'à ce que les tâches soient ajoutées/supprimées/mises à jour, même si nous lisons la liste des tâches terminées plusieurs fois.

Notez que nous n'avons pas besoin d'invalider manuellement le cache lorsque la liste de tâches change. Provider est automatiquement capable de savoir quand le résultat doit être recalculé grâce à ref.watch.

Réduire les reconstructions de provider/widgets en utilisant Provider.

Un aspect unique de Provider est que même lorsque Provider est recalculé (typiquement lors de l'utilisation de ref.watch), il ne mettra pas à jour les widgets/providers qui l'écoutent, sauf si la valeur a changé.

Un exemple concret serait d'activer/désactiver les boutons précédent/suivant d'une vue paginée :

stepper example

Dans notre cas, nous nous concentrerons spécifiquement sur le bouton "précédent". Une implémentation naïve d'un tel bouton serait un widget qui obtiendrait l'index de la page actuelle, et si cet index est égal à 0, nous désactiverions le bouton.

Ce code pourrait être :


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

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


Widget build(BuildContext context, WidgetRef ref) {
// si ce n'est pas sur la première page, le bouton précédent est actif
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'),
);
}
}

Le problème avec ce code est qu'à chaque fois que nous changeons la page en cours, le bouton "précédent" sera reconstruit. Dans un monde idéal, on voudrait que le bouton ne se reconstruise que lorsqu'il passe de l'état activé à l'état désactivé.

Le problème ici est que nous calculons si l'utilisateur est autorisé à aller à la page précédente directement à partir du bouton "précédent".

Une façon de résoudre ce problème est d'extraire cette logique en dehors du widget et dans un Provider :


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

// Un provider qui calcule si l'utilisateur est autorisé à aller à la page précédente.

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

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


Widget build(BuildContext context, WidgetRef ref) {
// On regarde maintenant notre nouveau Provider
// Notre widget ne calcule plus si on peut aller à la page précédente.

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

En effectuant cette petite refactorisation, notre widget PreviousButton ne sera plus reconstruit lorsque l'index de la page change grâce à Provider.

Dorénavant, lorsque l'index de la page change, notre provider canGoToPreviousPageProvider sera recalculé. Mais si la valeur exposée par le provider ne change pas, alors le PreviousButton ne sera pas reconstruit.

Ce changement a permis d'améliorer les performances de notre bouton et a eu l'avantage intéressant d'extraire la logique en dehors de notre widget.