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