Aller au contenu principal

Lire un Provider

Avant de lire ce guide, assurez-vous d'abord de lire sur les Providers.

Dans ce guide, nous verrons comment consommer un provider.

Obtention d'un objet "ref"

Tout d'abord, avant de lire un provider, nous devons obtenir un objet "ref".

Cet objet nous permet d'interagir avec les providers, que ce soit à partir d'un widget ou d'autres providers.

Obtention d'un "ref" depuis a provider

Tous les providers reçoivent un "ref" comme paramètre :

final provider = Provider((ref) {
// utiliser ref pour obtenir d'autres providers
final repository = ref.watch(repositoryProvider);

return SomeValue(repository);
})

Ce paramètre peut être passé à la valeur exposée par le provider sans danger.

Par exemple, un cas d'utilisation courant consiste à transmettre le "ref" du provider à 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 peut utiliser le "ref" pour lire d'autres providers
final repository = ref.read(repositoryProvider);
repository.post('...');
}
}

Cela permettrait à notre classe Counter de lire les providers.

Obtention d'un "ref" à partir d'un widget

Les widgets ne disposent naturellement pas d'un paramètre ref. Mais Riverpod propose plusieurs solutions pour en obtenir un, à partir des widgets.

Étendre de ConsumerWidget au lieu de StatelessWidget

La solution la plus courante consistera à remplacer StatelessWidget par ConsumerWidget. lors de la création d'un widget.

Le ConsumerWidget est fondamentalement identique au StatelessWidget, avec la seule différence est qu'elle possède un paramètre supplémentaire dans sa méthode de construction : l'objet "ref".

Une ConsumerWidget classique ressemblerait à ceci :


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


Widget build(BuildContext context, WidgetRef ref) {
// utiliser ref pour écouter un provider
final counter = ref.watch(counterProvider);
return Text('$counter');
}
}

Étendre ConsumerStatefulWidget+ConsumerState au lieu de StatefulWidget+State

Similaires à ConsumerWidget, ConsumerStatefulWidget et ConsumerState sont l'équivalent d'une StatefulWidget avec son State, à la différence que l'état (the state) est un objet "ref".

Cette fois, le "ref" n'est pas passé comme paramètre de la méthode build, mais est plutôt une propriété de l'objet ConsumerState:


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


HomeViewState createState() => HomeViewState();
}

class HomeViewState extends ConsumerState<HomeView> {

void initState() {
super.initState();
// "ref" peut être utilisé dans tous les cycles de vie d'un ConsumerStatefulWidget.
ref.read(counterProvider);
}


Widget build(BuildContext context) {
// On peut aussi utiliser "ref" pour écouter un provider dans la méthode de build.
final counter = ref.watch(counterProvider);
return Text('$counter');
}
}

Étendre HookConsumerWidget au lieu de HookWidget

Cette solution est spécifique aux utilisateurs de flutter_hooks. Étant donné que flutter_hooks nécessite d'étendre de HookWidget pour fonctionner. Les widgets qui utilisent les hooks sont incapables d'étendre ConsumerWidget.

Une solution consiste, à travers le package hooks_riverpod, à remplacer HookWidget par HookConsumerWidget.

Un exemple serait:


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


Widget build(BuildContext context, WidgetRef ref) {
// HookConsumerWidget permet d'utiliser des hooks dans la méthode de build.
final state = useState(0);

// On peut également utiliser le paramètre ref pour écouter les providers.
final counter = ref.watch(counterProvider);
return Text('$counter');
}
}

Les widgets Consumer et HookConsumer

Une dernière solution pour obtenir un "ref" à l'intérieur des widgets est de se servir de Consumer/HookConsumer.

Ces classes sont des widgets qui peuvent être utilisés pour obtenir un "ref", avec les mêmes propriétés que ConsumerWidget/HookConsumerWidget

Ces widgets peuvent être un moyen d'obtenir un "ref" sans avoir à définir une classe. Un exemple serait :

      body: HookConsumer(
builder: (context, ref, child) {
// Comme HookConsumerWidget, on peut utiliser des hooks dans le builder.
final state = useState(0);

// On peut également utiliser le paramètre ref pour écouter les providers.
final counter = ref.watch(counterProvider);
return Text('$counter');
},
),
);
}

Usage de ref pour interagir avec les providers

Maintenant que nous avons un "ref", on peut commencer à l'utiliser.

Il y a trois utilisations principales de "ref" :

  • Obtenir la valeur d'un provider et écouter les changements, de sorte que lorsque cette valeur change, cela reconstruira le widget ou le provider qui s'est abonné à la valeur. Cela peut être fait en utilisant ref.watch.
  • L'ajout d'un listener sur un provider, pour exécuter une action à chaque fois que ce provider change. Cela peut être fait en utilisant ref.listen.
  • Obtenir la valeur d'un provider tout en ignorant les changements. Cela est utile lorsque nous avons besoin de la valeur d'un provider dans un événement tel que "on click". Cela peut être fait en utilisant ref.read.
remarque

Quand c'est possible, il est préférable d'utiliser ref.watch plutôt que ref.read ou ref.listen pour implémenter une fonctionnalité.
En changeant votre implémentation pour qu'elle se base sur ref.watch, elle devient à la fois réactive et déclarative, ce qui rend votre application plus facile à maintenir.

Usage de ref.watch pour observer un provider

Il est possible d'utiliser ref.watch dans la méthode build d'un widget ou a l'interieur d'un provider pour que le widget/le provider écoute le provider.

Par exemple, un provider pourrait utiliser ref.watch pour combiner plusieurs providers en une nouvelle valeur.

Un exemple serait le filtrage d'une todo-list. On pourrait avoir deux providers:

  • filterTypeProvider, un provider qui expose (révèle) le type actuel du filtre (none, show only completed tasks, ...)
  • todosProvider, un provider qui expose (révèle) la liste des tâches.

Grâce à cela, en utilisant ref.watch, on peut créer un troisième provider qui combine les deux autre providers pour créer une liste filtrée de tâches :


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

final filteredTodoListProvider = Provider((ref) {
// récupère à la fois le filtre et la liste des todos
final FilterType filter = ref.watch(filterTypeProvider);
final List<Todo> todos = ref.watch(todosProvider);

switch (filter) {
case FilterType.completed:
// renvoie la liste complétée des todos
return todos.where((todo) => todo.isCompleted).toList();
case FilterType.none:
// renvoie la liste non filtrée des todos
return todos;
}
});

Avec ce code, filteredTodoListProvider expose (révèle) maintenant la liste filtrée des tâches.

La liste filtrée sera également mise à jour automatiquement si le filtre ou la liste de tâches a changée. Mais en même temps, la liste filtrée ne sera pas recalculée si ni le filtre ni la liste des tâches n'ont changé.

De la même manière, un widget pourrait utiliser ref.watch pour montrer une interface utilisateur qui dépend d'un provider:


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

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


Widget build(BuildContext context, WidgetRef ref) {
// utiliser ref pour écouter un provider
final counter = ref.watch(counterProvider);

return Text('$counter');
}
}

Ce snippet montre un widget qui écoute un provider qui stocke un compteur. Et si ce compteur change, le widget sera reconstruit et l'interface utilisateur sera mise à jour pour montrer la nouvelle valeur.

attention

La méthode watch ne doit pas être appelée de manière asynchrone, comme dans onPressed ou un ElevatedButton. Elle ne doit pas non plus être utilisée dans initState et d'autres cycles de vie de State.

Dans ces cas, pensez à utiliser ref.read à la place.

Usage de ref.listen pour réagir au changements d'un provider

De la même manière que ref.watch, il est possible d'utiliser ref.listen pour observer un provider.

La principale différence entre eux est que, plutôt que de reconstruire le widget/provider si le provider écouté change, l'utilisation de ref.listen appellera plutôt une fonction personnalisée.

Cela peut être utile pour effectuer des actions lorsqu'un certain changement se produit, par exemple d'afficher un snackbar lorsqu'une erreur se produit.

La méthode ref.listen a besoin de 2 arguments positionnels, le premier est le Provider et le second est la fonction callback que nous voulons exécuter lorsque l'état (state) change. La fonction callback, lorsqu'elle est appelée, renvoie deux valeurs, la valeur de l'état (state) précédent et la valeur du nouvel état (state).

La méthode ref.listen peut etre utilisée à l'intérieur d'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');
});
// ...
});

Ou à l'intérieur de la méthode build d'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();
}
}
attention

La méthode listen ne doit pas être appelée de manière asynchrone, comme dans onPressed ou un ElevatedButton. Elle ne doit pas non plus être utilisée dans initState et dans d'autres cycles de vie de State.

Usage de ref.read pour obtenir l'état d'un provider qu'une fois

La méthode ref.read est un moyen d'obtenir l'état d'un provider, sans aucun effet supplémentaire.

Il est généralement utilisé dans les fonctions déclenchées par les interactions de l'utilisateur. Par exemple, on peut utiliser ref.read pour incrémenter un compteur lorsqu'un utilisateur clique sur un bouton :


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: () {
// Appelez `increment()` sur la classe `Counter`.
ref.read(counterProvider.notifier).increment();
},
),
);
}
}
remarque

L'utilisation de ref.read doit être évitée autant que possible.

Il existe une solution pour contourner les cas où l'utilisation de watch ou listen serait trop peu pratique à utiliser. Si vous le pouvez, il est presque toujours préférable d'utiliser watch/listen, surtout watch.

NE PAS utiliser ref.read à l'intérieur de la méthode build

Vous pouvez être tenté d'utiliser ref.read pour optimiser les performances d'un widget en faisant :


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

Widget build(BuildContext context, WidgetRef ref) {
// utiliser "read" pour ignorer les mises à jour sur un provider
final counter = ref.read(counterProvider.notifier);
return ElevatedButton(
onPressed: () => counter.state++,
child: const Text('button'),
);
}

Mais ce n'est pas une bonne pratique et cela peut provoquer des bogues difficiles à repérer.

L'utilisation de ref.read de cette manière est généralement associée à la idée "La valeur exposée par un provider ne change jamais, donc utiliser 'ref.read' est sûr". Le problème avec cette supposition est que, si aujourd'hui ce provider peut en effet ne jamais mettre à jour sa valeur, rien ne garantit que ce sera le cas demain. la même chose.

Les softwares ont tendance à beaucoup changer, et il est probable qu'à l'avenir, une valeur qui n'a jamais changé devra être modifiée.

Mais si vous utilisiez ref.read, quand cette valeur commence à changer, vous allez devoir passer par toute votre codebase pour changer ref.read en ref.watch - ce qui est ce qui favorise les erreurs et il y a de fortes chances que vous en oubliiez certaines.

Alors que si vous utilisez ref.watch des le début, vous n'aurez aucun problème.

Mais je voulais utiliser ref.read pour réduire le nombre de fois où mon widget est reconstruit.

Bien que l'objectif soit méritant, il est important de noter que vous pouvez atteindre exactement le même effet (réduire le nombre de constructions) en utilisant ref.watch à la place.

Les providers offrent différents moyens d'obtenir une valeur tout en réduisant le nombre de reconstructions, que vous pourriez utiliser à la place.

Par exemple, au lieu de


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

On peut faire


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

On obtient les mêmes effets dans les deux exemples: notre bouton ne se reconstruira pas lorsque le compteur s'incrémente.

En revanche, la deuxième approche prend en charge les cas où le compteur est remis à zéro. Par exemple, une autre partie de l'application pourrait appeler :

ref.refresh(counterProvider);

ce qui recrée l'objet StateController.

Si nous avions utilisé ref.read ici, notre bouton utiliserait toujours la précédente instance de StateController précédente, qui a été éliminée et ne doit plus être utilisée. Alors que l'utilisation de ref.watch reconstruit correctement le bouton pour utiliser le nouveau StateController.

Décider ce qu'il faut lire

En fonction du provider que vous voulez écouter, vous pouvez avoir plusieurs valeurs possibles à écouter.

À titre d'exemple, considérons le StreamProvider suivant :

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

En lisant ce userProvider, vous pouvez :

  • lire de manière synchrone l'état actuel en écoutant le userProvider lui-même :

    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),
    );
    }
  • obtenir le Stream associé, en écoutant userProvider.stream :

    Widget build(BuildContext context, WidgetRef ref) {
    Stream<User> user = ref.watch(userProvider.stream);
    }
  • obtenir un Future qui sera la dernière valeur émise, en écoutant userProvider.last :

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

D'autres providers peuvent offrir des valeurs alternatives différentes. Pour plus d'informations, reportez-vous à la documentation de chaque provider en consultant la Référence API.

Usage de "select" pour filtrer les 'rebuilds'

Une dernière fonctionnalité à mentionner concernant la lecture des providers est la possibilité de réduire le nombre de reconstructions d'un widget/provider, ou combien de fois ref.listen exécute une fonction.

Il est important de garder cela à l'esprit car, par défaut, l'écoute d'un provider porte sur l'objet entier. Mais dans certains cas, un widget/provider peut ne s'intéresser qu'à certaines propriétés au lieu de l'objet entier.

Par exemple, un provider peut exposer (révèle) un User :

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

Mais un widget ne peut utiliser que le nom d'utilisateur :

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

Si nous avions utilisé naïvement ref.watch, cela aurait reconstruit le widget lorsque l'âge de "User" change.

La solution est d'utiliser select pour dire explicitement à Riverpod que nous voulons seulement écouter que certaines propriétés de "User".

Le nouveau code sera:

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

La façon dont cela fonctionne en utilisant select est la suivante : nous spécifions une fonction qui renvoie la propriété qui nous intéresse.

Ensuite, chaque fois que 'User' changera, Riverpod appellera cette fonction et comparera le résultat précédent et le nouveau. S'ils sont différents (par exemple lorsque le nom a changé), Riverpod reconstruira le widget. Mais s'ils sont égaux (comme lorsque l'âge a changé), Riverpod ne reconstruira pas le widget.

info

Il est aussi possible d'utiliser select avec ref.listen:

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

En faisant cela, le listener ne sera appelé que lorsque le nom changera.

astuce

Il n'est pas nécessaire de retourner une propriété de l'objet. Toute valeur qui override == fonctionnera. Par exemple, vous pouvez faire :

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