Skip to content
Snippets Groups Projects
Commit bbf958ca authored by Adrien BONNIN's avatar Adrien BONNIN
Browse files

:bookmark: Version 1.1

parent 8fa918a1
No related branches found
No related tags found
No related merge requests found
# Découvrir Riverpod par la pratique
# Riverpod par la pratique : découverte des bases
Cet article a pour objectif de faire découvrir pas à pas la bibliothèque Riverpod à partir d'exemples d'utilisation.
Cet article a pour objectif de vous faire découvrir pas à pas la bibliothèque Riverpod à partir d'exemples d'utilisation.
## Introduction
## Il était une fois la gestion d’états
Riverpod est une bibliothèque de state management pour Flutter.
Le state management, ou gestion d'état en français, a pour responsabilité de mettre à disposition les différents objets qui constituent l'état de l'application.
Le terme _"état"_, employé tout au long de cet article, fait précisément référence à ces états.
Riverpod est une bibliothèque de gestion d’états pour Flutter.
La _"gestion d’états"_, ou _"state management"_ en anglais, a pour responsabilité de mettre à disposition les différents objets qui constituent l'état de l'application.
La principale particularité de Flutter est que tout est widget.
Le state management ne faisant pas exception, de nombreuses bibliothèques sont apparues proposant chacune leurs propres patterns et méthodes de fonctionnement.
La gestion d’états ne faisant pas exception, de nombreuses bibliothèques sont apparues proposant chacune leurs propres patterns et méthodes de fonctionnement.
Parmi les plus utilisées on peut citer [BLoC](https://pub.dev/packages/bloc), [GetX](https://pub.dev/packages/get), [Provider](https://pub.dev/packages/provider) et aujourd'hui c'est [Riverpod](https://pub.dev/packages/riverpod) qui nous intéresse.
Le créateur et principal contributeur de Riverpod, Remi Rousselet, n'en est pas à son premier ballon d'essai.
Reconnu au sein de la communauté Dart et Flutter, il est entre autre l'auteur de [freezed](https://pub.dev/packages/freezed) (un générateur de code pour les data-classes et unions), [flutter_hooks](https://pub.dev/packages/flutter_hooks) (une implémentation des hooks React) et [Provider](https://pub.dev/packages/provider) (un autre state management).
Cette dernière est le point de départ d'une réflexion plus ambitieuse sur le state management qui impliquera une réécriture complète pour aboutir à Riverpod.
Reconnu au sein de la communauté Dart et Flutter, il est entre autre l'auteur de [freezed](https://pub.dev/packages/freezed) (un générateur de code pour les data-classes et unions), [flutter_hooks](https://pub.dev/packages/flutter_hooks) (une implémentation des hooks React) et [Provider](https://pub.dev/packages/provider) (une autre gestion d’états).
Cette dernière est le point de départ d'une réflexion plus ambitieuse sur la gestion d'états qui impliquera une réécriture complète pour aboutir à Riverpod.
Cet article fait référence à la bibliothèque Provider sous ces termes uniquement pour ne pas la confondre avec la classe `Provider` présente dans Riverpod.
## Installation
## En route avec Riverpod !
Dès son installation, Riverpod se distingue de ses concurrentes par son découpage en plusieurs bibliothèques :
- [`riverpod`](https://pub.dev/packages/riverpod) contient le code principale, sans aucune adhérence.
- [`riverpod_flutter`](https://pub.dev/packages/flutter_riverpod) contient le code spécifique pour le framework Flutter.
- [`hooks_riverpod`](https://pub.dev/packages/hooks_riverpod) contient le code spécifique pour la bibliothèque [`flutter_books`](https://pub.dev/packages/flutter_hooks).
- [`hooks_riverpod`](https://pub.dev/packages/hooks_riverpod) contient le code spécifique pour la bibliothèque [`flutter_hooks`](https://pub.dev/packages/flutter_hooks).
La bibliothèque `flutter_riverpod` sera utilisée pour aborder l'ensemble des fondamentaux de Riverpod.
La dépendance est à ajouter dans le fichier pubspec.yaml :
......@@ -69,12 +68,12 @@ void main() {
}
```
![Provider](docs/images/provider.png)
![provider](docs/images/provider.png)
Le `Provider` est instancié en tant que variable globale et son état est initialisé avec la valeur 13 `<1>`.
L'instance du `ProviderContainer` contient les états `<2>` et permet de les récupérer en passant l'instance du `Provider` en paramètre de sa méthode `read` `<3>`.
Déclarer le `Provider` en tant que variable globale `<1>` peut sembler une erreur de conception mais il n'en est rien.
Déclarer le `Provider` en tant que variable globale `<1>` peut sembler une erreur de conception, mais il n'en est rien.
Il est avant tout immutable et ne contient pas un état, mais constitue un __moyen de le récupérer__.
La visibilité globale est alors un choix judicieux pour le rendre disponible n'importe où dans le code.
......@@ -83,18 +82,105 @@ Cette erreur est levée quand un état est accédé alors qu'il n'a pas été en
Le provider étant responsable de l'initialisation de l'état, ce dernier sera systématiquement initialisé avant d'être récupéré.
> Le contrat du `Provider` se limite à la récupération d'un état.
> Il est __trés fortement recommandé__ d'utiliser des objets immutables pour définir les états, les modifications internes sont à proscrire et ne seront pas notifiées.
> Les modifications internes de l’état ne sont pas notifiées pour favoriser l’utilisation d’objets immutables.
Plus généralement, l'immutabilité des objets échangés est, entre autres, une bonne pratique dans le développement événementiel car elle limite les effets de bord.
Plus généralement, l'immutabilité des objets échangés est, entre autres, une bonne pratique dans le développement événementiel, car elle limite les effets de bord.
La déclaration de ces objets peut se révéler fastidieuse en Dart et c'est ce que propose de simplifier la bibliothèque [freezed](https://pub.dev/packages/freezed).
## Test, test, test and test
## Le cycle de vie des Providers
Étant donné que le `ProviderContainer` contient les états, en instancier un nouveau pour chaque test permet de garantir leur isolation.
La récupération de son état n'est qu'une partie du cycle de vie d'un `Provider`.
Le paramètre `ProviderRef`, présent dans l'initialisation de chaque `Provider`, propose par sa méthode `onDispose` de définir une callback qui sera appelée pour libérer ses ressources.
[source : circle_of_life.dart](https://gitlab.ippon.fr/adbonnin/article_flutter_riverpod/-/blob/main/test/circle_of_life.dart)
```dart
void main() {
test('doit vérifier le cycle de vie du Provider', () {
const defaultValue = -1;
// given:
final container = ProviderContainer();
var intValue = defaultValue; // <1>
var disposed = false;
En plus de contenir les états, le `ProviderContainer` gère aussi leurs cycles de vie et sa méthode `dispose` libère ces ressources à la fin des tests.
final intProvider = Provider.autoDispose<int>((ref) {
ref.onDispose(() => disposed = true);
return intValue = 13;
});
Pour homogénéiser leurs écritures et éviter d'éventuels oublis de libération de ressources, la déclaration des `ProviderContainer` peut être factorisée comme le fait Riverpod avec [la méthode createContainer](https://github.com/rrousselGit/riverpod/blob/v1.0.4/packages/flutter_riverpod/test/utils.dart#L11).
// expect:
expect(intValue, equals(defaultValue)); // <2>
expect(disposed, isFalse);
// when:
container.read(intProvider); // <3>
// then:
expect(intValue, equals(13)); // <4>
expect(disposed, isFalse);
// when:
container.dispose(); // <5>
// then:
expect(disposed, isTrue); // <6>
});
}
```
![Circle of life](docs/images/circle_of_life.png)
Le `Provider` est déclaré au sein du test pour retenir son affectation d'état et sa libération de ressources `<1>`.
Avant toute manipulation, sa valeur n'est toujours pas affectée `<2>`.
Ce n'est qu'après avoir été lu `<3>` que sa valeur est initialisée `<4>`.
Les ressources du `Provider` sont libérées `<6>` avec celles du `ProviderContainer` `<5>`.
> En plus de contenir les états, le `ProviderContainer` gère aussi leurs cycles de vie.
> Un état ne sera initialisé qu'à partir du moment où il sera lu, reflétant la nature paresseuse (lazy) du `ProviderContainer`.
Pour des questions de consommation mémoire, il est parfois nécessaire d'écourter la vie d'un `Provider` en libérant ses ressources dès qu'il n'est plus utilisé.
Ce comportement additionnel du `Provider` est à ajouter dès sa déclaration avec le _modifier_ `autodispose` :
[source : autodispose.dart](https://gitlab.ippon.fr/adbonnin/article_flutter_riverpod/-/blob/main/test/autodispose.dart)
```dart
void main() {
test('doit vérifier que les ressources du Provider sont libérées', () async {
// given:
final container = ProviderContainer();
addTearDown(container.dispose);
var disposed = false; // 1
final provider = Provider.autoDispose<void>((ref) {
ref.onDispose(() => disposed = true);
});
// when:
container.read(provider); // <2>
// then:
expect(disposed, isFalse); // <3>
// when:
await Future.delayed(const Duration(milliseconds: 1)); // <4>
// then:
expect(disposed, isTrue); // <5>
});
}
```
![Provider](docs/images/autodispose.png)
Le `Provider` est déclaré pour que ses ressources soient libérées dès qu'il n'est plus utilisé `<1>`.
Après avoir été lu `<2>` ses ressources ne sont pas instantanément libérées `<3>`.
Ce n'est qu'après avoir lâché la main sur la boucle synchrone `<4>` que la libération asynchrone de ses ressources est effective `<5>`.
> Un `Provider` déclaré avec le _modifier_ `autodispose` libère son état de manière asynchrone dès qu'il n'est plus utilisé.
## Test, test, test and test
Pour homogénéiser l'écriture des tests et éviter d'éventuels oublis de libération de ressources, la déclaration des `ProviderContainer` peut être factorisée comme le fait Riverpod avec [la méthode createContainer](https://github.com/rrousselGit/riverpod/blob/v1.0.4/packages/flutter_riverpod/test/utils.dart#L11).
Cette méthode sera par la suite utilisée dans les prochains tests de cet article.
[_source : utils.dart_](https://gitlab.ippon.fr/adbonnin/article_flutter_riverpod/-/blob/main/test/utils.dart)
......@@ -117,11 +203,13 @@ ProviderContainer createContainer({
}
```
La variable `overrides` présente dans cette factory de `ProviderContainer` va se révéler particulièrement utile pour préparer la situation initiale des tests.
Étant donné que le `ProviderContainer` contient les états, en instancier un nouveau pour chaque test permet de garantir leur isolation.
La variable `overrides` passée à l'initialisation du `ProviderContainer` va se révéler particulièrement utile pour préparer la situation initiale des tests.
La liste d'`Override`s qu'elle contient vient surcharger le comportement du `ProviderContainer`.
Ces `Override`s sont retournés par les méthodes idoines du `Provider`, `overrideWithProvider` pour le remplacer et `overrideWithValue` pour remplacer l'état récupéré.
[_source : test_overrides.dart_](https://gitlab.ippon.fr/adbonnin/article_flutter_riverpod/-/blob/main/test/test_overrides.dart)
[_source : overrides.dart_](https://gitlab.ippon.fr/adbonnin/article_flutter_riverpod/-/blob/main/test/overrides.dart)
```dart
final intProvider = Provider<int>((_) => 13); // <1>
final otherIntProvider = Provider<int>((_) => 13);
......@@ -149,6 +237,35 @@ Deux `Providers` sont déclarés pour retourner la valeur 13 `<1>`.
Le comportement du `ProviderContainer` est surchargé pour remplacer le premier `Provider` et pour remplacer la valeur retournée par le second `<2>`.
La valeur récupérée par les deux est maintenant de 42 `<3>`.
## Distinction des états pour un Provider
Un même `Provider` peut se voir confié plusieurs états en attribuant à chacun une clef.
Le _modifier_ `family` ajoute ce comportement à la déclaration du `Provider` en définissant le type de la clef avec son générique :
```dart
final evenProvider = StateProvider.family<bool, int>((_, arg) => arg.isEven); // <1>
void main() {
test('doit distinguer chaque état dans un provider', () {
// given:
final container = createContainer();
// then:
expect(container.read(evenProvider(13)), isFalse); // <2>
expect(container.read(evenProvider(42)), isTrue);
});
}
```
![.family](docs/images/family.png)
Un `Provider` est déclaré pour indiquer par ses états si la clef passée en paramètre est paire `<1>`.
La lecture du `Provider` se fait en passant cette clef et la valeur correspondante est retournée `<2>`.
Pour simplifier, le _modifier_ `family` transforme le `Provider` en `Map` avec comme clefs son générique et comme valeurs les états correspondants.
> Un `Provider` déclaré avec le _modifier_ `family` dispose d'autant d'états que de clef qui leur sont attribuées.
## Changer d'état avec le StateProvider
Le `Provider` est un moyen de récupérer un état, mais en aucun cas de le modifier.
......@@ -175,7 +292,7 @@ void main() {
}
```
![Provider](docs/images/state_provider.png)
![StateProvider](docs/images/state_provider.png)
Le `StateProvider` est instancié de la même manière qu'un `Provider`, en initialisant son état avec la valeur 13 `<1>`.
Le notifier du `StateProvider` est récupéré par l'intermédiaire du `ProviderContainer` et son état est remplacé par la valeur 42 `<2>`.
......@@ -193,11 +310,9 @@ D'un point de vue conceptuel, l'unique interaction que partagent toutes les clas
Cependant, chaque classe de `Provider` dispose de son propre contrat et c'est par l'intermédiaire de providers additionnels que les comportements sont adaptés.
Dans le cas du `StateProvider`, c'est le provider additionnel `notifier` qui ajoute le changement de valeur.
## Surveillance des Providers
## Surveillance entre Providers
En plus de lire l'état d'un `Provider`, il est possible de surveiller ses changements.
Par souci de simplicité, le paramètre `ProviderRef`, présent dans l'initialisation de chaque `Provider`, avait été jusque-là ignoré.
C'est par son intermédiaire qu'un `Provider` peut lire et/ou surveiller d'autres `Provider`s et ainsi former un graphe de dépendances.
Un `Provider` peut surveiller un autre `Provider` dès son initialisation par l'intermédiaire de la méthode `watch` du paramètre `ProviderRef` :
[_source : watch.dart_](https://gitlab.ippon.fr/adbonnin/article_flutter_riverpod/-/blob/main/test/watch.dart)
```dart
......@@ -224,20 +339,21 @@ void main() {
}
```
![Provider](docs/images/watch.png)
![watch modifier](docs/images/watch.png)
Le `StateProvider` `intProvider` est initialisé avec la valeur 13, `readProvider` vient lire son état et `watchProvider` le surveiller `<1>`.
Lors de leurs première lecture, l'état des `readProvider` et `watchProvider` sont identiques à celui du `intProvider` `<2>`.
Lors de leurs premières lectures, l'état des `readProvider` et `watchProvider` sont identiques à celui du `intProvider` `<2>`.
L'état du `intProvider` est modifié avec la valeur 42 `<3>` et seulement `watchProvider` prend en compte ce changement `<4>`.
Le comportement des méthodes `read` et `watch` est identique lors de la première initialisation, l'état du `intProvider` est récupéré pour être ensuite retourné.
C'est lors de la modification du intProvider `<3>` que les comportements divergent.
C'est lors de la modification du `intProvider` `<3>` que les comportements divergent.
Le `readProvider` ne se préoccupe pas de cette nouvelle valeur alors que la `watchProvider` vient appeler de nouveau sa méthode d'initialisation `<1>` pour mettre à jour son état en adéquation avec celui du `intProvider`.
> Le `ProviderRef` peut être concidéré comme une façade au `ProviderContainer`.
> Le comportement attendu de ses méthodes `read` et `watch` est identique selon que l'on l'utilise `ProviderContainer` dans le corps du test ou `ProviderRef` dans l'initialisation du `Provider`.
> Un `Provider` peut écouter un autre `Provider` dès son utilisation pour former un graphe de dépendances.
> Le `ProviderRef` peut être considéré comme une façade au `ProviderContainer`.
> Le comportement attendu des méthodes `read` et `watch` est identique selon que l'on utilise un `ProviderContainer` dans le corps du test ou un `ProviderRef` dans l'initialisation du `Provider`.
Ce fonctionnement est illustré par le code ci-dessous tiré de Riverpod :
Ce fonctionnement en façade est illustré par le code ci-dessous tiré de Riverpod :
[_source : provider_base.dart_](https://github.com/rrousselGit/riverpod/blob/v1.0.4/packages/riverpod/lib/src/framework/provider_base.dart#L657)
```dart
......@@ -250,9 +366,9 @@ T read<T>(ProviderBase<T> provider) {
}
```
A noter que la surveillance vient créer un lien de dépendance entre les `Provider`s et peut mener à l'apparition de dépendances cycliques :
À noter que la surveillance vient créer un lien de dépendance entre les `Provider`s et peut mener à l'apparition de dépendances circulaires :
[_source : dependance_cyclique.dart_](https://gitlab.ippon.fr/adbonnin/article_flutter_riverpod/-/blob/main/test/dependance_cyclique.dart)
[_source : circular_dependency.dart_](https://gitlab.ippon.fr/adbonnin/article_flutter_riverpod/-/blob/main/test/circular_dependency.dart)
```dart
final Matcher throwsProviderException = throwsA(const TypeMatcher<ProviderException>());
......@@ -260,7 +376,7 @@ final Provider<int> provider = Provider<int>((ref) => ref.watch(otherProvider));
final Provider<int> otherProvider = Provider<int>((ref) => ref.watch(provider));
void main() {
test('doit lever une exception suite à une dépendance cyclique', () {
test('doit lever une exception suite à une dépendance circulaires', () {
// given:
final container = createContainer();
......@@ -272,7 +388,7 @@ void main() {
Une interdépendance est déclarée entre deux `Provider` `<1>` et à la lecture de l'un d'entre eux une exception est levée `<2>`.
> Riverpod dispose d'un mécanisme qui vient lever une exception quand une dépendance cyclique est détectée.
> Riverpod dispose d'un mécanisme qui vient lever une exception quand une dépendance circulaire est détectée.
## Le listener qui écoutait à l'oreille des Providers
......@@ -318,7 +434,7 @@ void main() {
}
```
![Provider](docs/images/listener.png)
![listener](docs/images/listener.png)
Le `StateProvider` `intProvider` est initialisé avec la valeur 13 et `watchProvider` surveille ses modifications `<1>`.
Des listeners écoutent leurs changements d'état respectif pour stocker les nouvelles valeurs `<2>`.
......@@ -330,13 +446,14 @@ Ce n'est qu'après la lecture du `watchProvider` que son listener est notifié `
Écouter un provider avec la méthode `listen` permet d'être notifié lors d'un changement d'état.
Ce changement d'état ne sera effectif qu'à partir du moment où il sera lue et non à partir du moment où il a été modifié, c'est un __fonctionnement passif__.
C'est pour cette raison que la valeur de `watchValue` reste à -1 `<6>`.
C'est pour cette raison que la valeur de `watchValue` conserve sa valeur par défaut `<6>`.
Surveiller un provider avec la méthode `watch` vient lire le nouvel état à la suite d'un changement.
Ce changement d'état est effectif dès sa modification, c'est un __fonctionnement actif__.
Cependant, le fonctionnement paresseux du `ProviderContainer` ne propagera le changement `<7>` qu'à partir du moment où le `Provider` sera lu `<6>`.
> Riverpod intialise les états de manière paresseuse (lazy).
> Un état ne sera initialisé qu'à partir du moment où il sera lu, par un `read` ou un `watch`.
> Écouter est un fonctionnement passif alors que surveiller est un fonctionnement actif.
> En plus d'être paresseux à l'initialisation des états, le `ProviderContainer` l'est également lors leurs modifications.
## Devenir sélectif dans les changements d'états
......@@ -378,7 +495,7 @@ void main() {
}
```
![Provider](docs/images/select.png)
![select](docs/images/select.png)
Le `StateProvider` `intProvider` est initialisé avec la valeur 13 et le `moduloProvider` vient l'écouter en sélectionnant uniquement le modulo de 10 de l'état `<1>`.
Un compteur écoute le nombre de changements réalisés par `moduloProvider` `<2>`.
......@@ -386,7 +503,7 @@ Initialement le modulo de 13 vaut 3 et aucun changement n'est encore réalisé `
Suite à la modification de la valeur de `intProvider` par 42 `<4>`, le modulo vaut 2 et un changement est ajouté au compteur `<5>`.
La valeur du `intProvider` est à nouveau modifiée avec la valeur 22 `<6>` mais étant donné que son modulo est identique à celui de 42 aucune modification n'est apportée à l'état du `moduloProvider` `<7>`.
A noter que cet exemple n'a vocation qu'à présenter la théorie : l'utilisation du `select` dans l'initialisation d'un provider ne présente aucun intérêt étant donné que le comportement est identique à celui d'un `Provider` effectuant lui même l'opération :
À noter que cet exemple n'a vocation qu'à présenter la théorie : l'utilisation du `select` dans l'initialisation d'un provider ne présente aucun intérêt étant donné que le comportement est identique à celui d'un `Provider` effectuant lui-même l'opération :
```dart
final moduloProvider = Provider<int>((ref) => ref.watch(intProvider) % 10);
......@@ -455,23 +572,23 @@ void main() {
}
```
![Provider](docs/images/state_notifier_provider.png)
![state_notifier_provider](docs/images/state_notifier_provider.png)
Le `StateProvider` `intProvider` est initialisé avec la valeur 13 `<1>`.
Il est écouté par le `StateNotifierProvider` `incrementProvider` dont le notifier `IncrementNotifier` dispose d'une méthode pour incrémenté son état `<2>`.
Il est écouté par le `StateNotifierProvider` `incrementProvider` dont le notifier `IncrementNotifier` dispose d'une méthode pour incrémenter son état `<2>`.
L'état récupéré par le `incrementProvider` est bien celui du `intProvider` `<3>`.
L'appel à la méthode `increment` `<4>` fait passer la valeur du `incrementProvider` de 13 à 14 `<5>`.
Après avoir modifier l'état du `intProvider`, le `incrementProvider` est initialisé à nouveau et prend la valeur 42.
Après avoir modifié l'état du `intProvider`, le `incrementProvider` est initialisé à nouveau et prend la valeur 42.
> Le contrat du `StateNotifierProvider` va de la récupération à la modification de l'état.
> La classe `StateNotifier` est à étendre en indiquant le type du state en générique et de passer sa valeur initiale au constructeur parent.
> La classe `StateNotifier` est à étendre en indiquant le type de l'état en générique et de passer sa valeur initiale au constructeur parent.
> L'état est stocké dans la variable `state` avec une visibilité limitée en `protected` pour conserver une implémentation étanche.
> Les modifications sont réalisées en affectant un nouvel objet à la variable `state`.
> Chacune de ces modifications est ensuite notifiée aux objets qui le surveillent.
## ChangeNotifierProvider, le vilain petit canard
Jusqu'à présent les états se devaient d'être immutables mais pour des questions de performances ou de conception il est parfois nécessaire d'abandonner cette bonne pratique.
Jusqu'à présent les états se devaient d'être immutables mais, pour des questions de performances ou de conception, il est parfois nécessaire d'abandonner cette bonne pratique.
Le `ChangeNotifierProvider` répond à ce cas de figure en laissant à la charge du développeur de notifier les changements apportés à l'état :
```dart
......@@ -531,7 +648,7 @@ void main() {
}
```
![Provider](docs/images/change_notifier_provider.png)
![change_notifier_provider](docs/images/change_notifier_provider.png)
La classe `IncrementNotifier`, un `ChangeNotifier` avec une méthode `increment` pour incrémenter sa propriété `number`, est initialisé avec la valeur 13 `<1>`.
La propriété `number` de son `notifier` dispose bien de la valeur 13.
......@@ -539,20 +656,19 @@ Après avoir été incrémentée `<3>`, la valeur prend la valeur 14 `<4>`.
Aucune séparation n'étant faite entre le __contenant__ et le __contenu__ du `ChangeNotifier`, il est retourné par le `ProviderContainer` aussi bien en tant qu'état `<2>`, qu'en tant que provider additionnel `notifier` `<4>`.
A noter que la classe `ChangeNotifier` est initialement proposée par Flutter pour fournir un mécanisme d'écoute et de notification.
Le `ChangeNotifierProvider` est donc parfois utile pour migrer d'anciennes applications utilisant le `ChangeNotifier` comme state management.
À noter que la classe `ChangeNotifier` est initialement proposée par Flutter pour fournir un mécanisme d'écoute et de notification.
Le `ChangeNotifierProvider` est donc parfois utile pour migrer d'anciennes applications utilisant le `ChangeNotifier` comme gestion d'états.
## Détour sur le Future avec le FutureProvider
La classe `Future` et Riverpod partagent un même monde que tout oppose.
Le `Future` est mutable et utilise des callbacks alors que Riverpod prône l'immutabilité et utilise des états.
Le `FutureProvider` est là pour les réconcilier autour de l'`AsyncValue`.
Ces deux conceptions, radicalement opposées, deviennent compatible en adaptant le `Future` par un `FutureProvider`.
L'interface[^1] `AsyncValue` proposée par Riverpod reflète par ses différentes implémentations les états que prend un `Future`.
L'état de chargement, de retour de la donnée et d'erreur sont respectivement représentés par les factories `AsyncValue.loading`, `AsyncValue.data` et `AsyncValue.error`.
À cela vient s'ajouter la méthode statique `AsyncValue.guard` pour transformer un `Future` en `AsyncValue`.
Le `FutureProvider` retourne l'interface[^1] `AsyncValue` dont les implémentations traduisent en états immutables les différentes étapes d'un `Future`.
Le chargement, les callbacks de retour et d'erreur sont respectivement représentés par ses factories `AsyncValue.loading`, `AsyncValue.data` et `AsyncValue.error`.
À cela vient s'ajouter la méthode statique `AsyncValue.guard` pour adapter une fonction retournant un `Future` en `Future` d'`AsyncValue`.
Pour faciliter son utilisation, `AsyncValue` dispose des mêmes méthodes que [les unions de Freezed](https://pub.dev/packages/freezed#union-types-and-sealed-classes) avec un équivalent au pattern matching avec [l'extension `AsyncValueX`](https://pub.dev/documentation/riverpod/latest/riverpod/AsyncValueX.html).
L'utilisation de l`AsyncValue` est simplifiée par les méthodes de son [extension `AsyncValueX`](https://pub.dev/documentation/riverpod/latest/riverpod/AsyncValueX.html), inspirée par [les unions de freezed](https://pub.dev/packages/freezed#union-types-and-sealed-classes), en proposant un équivalent au pattern matching.
[_source : future_provider.dart_](https://gitlab.ippon.fr/adbonnin/article_flutter_riverpod/-/blob/main/test/future_provider.dart)
```dart
......@@ -579,7 +695,7 @@ void main() {
}
```
![Provider](docs/images/future_provider.png)
![future_provider](docs/images/future_provider.png)
Un `FutureProvider` est instancié pour que son état prenne la valeur 13 après 100ms `<1>`.
Tant que le `Futur` n'a pas été résolu, aucune valeur n'est attribuée à l'état et il conserve la valeur `AsyncLoading` `<2>`.
......@@ -626,17 +742,20 @@ void main() {
}
```
![Provider](docs/images/stream_provider.png)
![stream_provider](docs/images/stream_provider.png)
Une `Stream` est déclarée pour retourner la valeur 13 suivie d'une erreur, le tout entrecoupé par un délai de 100 millisecondes `<1>`.
Tant que la première valeur de la `Stream` n'est pas retournée, l'état conserve la valeur `AsyncLoading` `<2>`.
Après avoir attendu 150ms `<3>`, l'état devient un `AsynData` avec pour valeur 13 `<4>`.
Une exception est levée 100ms plus tard `<5>` et l'état retourne une `AsyncError` `<6>`.
## Conclusion
## L'aventure ne fait que commencer...
Ainsi se termine ce premier article sur Riverpod.
Vous disposez maintenant des bases pour en comprendre les principaux mécanismes.
Le prochain article sera plus court et portera sur son intégration avec Flutter.
[^1] Les interfaces n'existent pas à proprement parler en Dart, ce sont des classes abstraites avec des méthodes abstraites dont l'interface implicite est implementée.
\ No newline at end of file
![zelda_sword](docs/images/zelda_sword.png)
[^1]: Les interfaces n'existent pas à proprement parler en Dart, ce sont des classes abstraites avec des méthodes abstraites dont l'interface implicite est implémentée.
\ No newline at end of file
docs/images/autodispose.png

4.12 KiB

docs/images/circle_of_life.png

8.29 KiB

docs/images/family.png

3.29 KiB

No preview for this file type
docs/images/zelda_sword.png

8.38 KiB

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
test('doit vérifier que les ressources du Provider sont libérées', () async {
// given:
final container = ProviderContainer();
addTearDown(container.dispose);
var disposed = false; // 1
final provider = Provider.autoDispose<void>((ref) {
ref.onDispose(() => disposed = true);
});
// when:
container.read(provider); // <2>
// then:
expect(disposed, isFalse); // <3>
// when:
await Future.delayed(const Duration(milliseconds: 1)); // <4>
// then:
expect(disposed, isTrue); // <5>
});
}
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
test('doit vérifier le cycle de vie du Provider', () {
const defaultValue = -1;
// given:
final container = ProviderContainer();
addTearDown(container.dispose);
var intValue = defaultValue; // <1>
var disposed = false;
final intProvider = Provider.autoDispose<int>((ref) {
ref.onDispose(() => disposed = true);
return intValue = 13;
});
// expect:
expect(intValue, equals(defaultValue)); // <2>
expect(disposed, isFalse);
// when:
container.read(intProvider); // <3>
// then:
expect(intValue, equals(13)); // <4>
expect(disposed, isFalse);
// when:
container.dispose(); // <5>
// then:
expect(disposed, isTrue); // <6>
});
}
......@@ -9,7 +9,7 @@ final Provider<int> provider = Provider<int>((ref) => ref.watch(otherProvider));
final Provider<int> otherProvider = Provider<int>((ref) => ref.watch(provider));
void main() {
test('doit lever une exception suite à une dépendance cyclique', () {
test('doit lever une exception suite à une dépendance circulaire', () {
// given:
final container = createContainer();
......
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'utils.dart';
final evenProvider = StateProvider.family<bool, int>((_, arg) => arg.isEven); // <1>
void main() {
test('doit distinguer chaque état dans un provider', () {
// given:
final container = createContainer();
// then:
expect(container.read(evenProvider(13)), isFalse); // <2>
expect(container.read(evenProvider(42)), isTrue);
});
}
\ No newline at end of file
File moved
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment