diff --git a/README.md b/README.md index f20313d240c91193e2b30d4b4f04f645b4d525e6..1cbae49ae39b48c9c6431cb473108d88a8afcbcf 100644 --- a/README.md +++ b/README.md @@ -1,92 +1,642 @@ -# article_flutter_riverpod +# Découvrir Riverpod par la pratique +Cet article a pour objectif de faire découvrir pas à pas la bibliothèque Riverpod à partir d'exemples d'utilisation. +## Introduction -## Getting started +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. -To make it easy for you to get started with GitLab, here's a list of recommended next steps. +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. +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. -Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)! +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. +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. -## Add your files +## Installation + +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). -- [ ] [Create](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files -- [ ] [Add files using the command line](https://docs.gitlab.com/ee/gitlab-basics/add-file.html#add-a-file-using-the-command-line) or push an existing Git repository with the following command: +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 : +```yaml +dependencies: + flutter_riverpod: ^1.0.3 ``` -cd existing_repo -git remote add origin https://gitlab.ippon.fr/adbonnin/article_flutter_riverpod.git -git branch -M main -git push -uf origin main + +## Une histoire de Provider + +L'opération la plus élémentaire est de récupérer un état stocké dans un `ProviderContainer` par l'intermédiaire d'un `Provider`. + +Le `ProviderContainer` est un conteneur d'états. +Pour simplifier, c'est une `Map` avec comme clefs les __instances des providers__ et comme valeurs les états correspondants. +Utiliser des instances comme clef corrige la limitation de la bibliothèque Provider qui ne supporte qu'une valeur par classe. + +Le `Provider` est un moyen de __récupérer__ un état présent dans un `ProviderContainer`. +La déclaration d'un provider permet d'indiquer __le type__ et __la valeur d'initialisation__ de l'état auquel il correspond. + + + +> Déclarer un provider permet de __typer__, __initialiser__ et __récupérer__ un état. + +La récupération d'un état est réalisée de la manière suivante : + +[_source : provider.dart_](https://gitlab.ippon.fr/adbonnin/article_flutter_riverpod/-/blob/main/test/provider.dart) +```dart +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; + +final intProvider = Provider<int>((_) => 13); // <1> + +void main() { + test('doit récupérer un état', () { + // given: + final container = ProviderContainer(); // <2> + addTearDown(container.dispose); + + // expect: + expect(container.read(intProvider), equals(13)); // <3> + }); +} ``` -## Integrate with your tools + -- [ ] [Set up project integrations](https://gitlab.ippon.fr/adbonnin/article_flutter_riverpod/-/settings/integrations) +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>`. -## Collaborate with your team +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. -- [ ] [Invite team members and collaborators](https://docs.gitlab.com/ee/user/project/members/) -- [ ] [Create a new merge request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html) -- [ ] [Automatically close issues from merge requests](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically) -- [ ] [Enable merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/) -- [ ] [Automatically merge when pipeline succeeds](https://docs.gitlab.com/ee/user/project/merge_requests/merge_when_pipeline_succeeds.html) +Une autre particularité du `Provider` est de résoudre la principale erreur rencontrée par la bibliothèque `Provider`, à savoir le `ProviderNotFoundException`. +Cette erreur est levée quand un état est accédé alors qu'il n'a pas été encore initialisé. +Le provider étant responsable de l'initialisation de l'état, ce dernier sera systématiquement initialisé avant d'être récupéré. -## Test and Deploy +> 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. -Use the built-in continuous integration in GitLab. +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). -- [ ] [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/index.html) -- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing(SAST)](https://docs.gitlab.com/ee/user/application_security/sast/) -- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html) -- [ ] [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/) -- [ ] [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html) +## Test, test, test and test -*** +Étant donné que le `ProviderContainer` contient les états, en instancier un nouveau pour chaque test permet de garantir leur isolation. -# Editing this README +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. -When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thank you to [makeareadme.com](https://www.makeareadme.com/) for this template. +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). +Cette méthode sera par la suite utilisée dans les prochains tests de cet article. -## Suggestions for a good README -Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information. +[_source : utils.dart_](https://gitlab.ippon.fr/adbonnin/article_flutter_riverpod/-/blob/main/test/utils.dart) +```dart +import 'package:flutter_test/flutter_test.dart'; +import 'package:riverpod/riverpod.dart'; -## Name -Choose a self-explaining name for your project. +ProviderContainer createContainer({ + ProviderContainer? parent, + List<Override> overrides = const [], + List<ProviderObserver>? observers, +}) { + final container = ProviderContainer( + parent: parent, + overrides: overrides, + observers: observers, + ); + addTearDown(container.dispose); + return container; +} +``` -## Description -Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors. +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. +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) +```dart +final intProvider = Provider<int>((_) => 13); // <1> +final otherIntProvider = Provider<int>((_) => 13); + +void main() { + test('doit surcharger le comportement des Provider', () { + // given: + final container = createContainer( + overrides: [ + intProvider.overrideWithProvider(Provider<int>((_) => 42)), // <2> + otherIntProvider.overrideWithValue(42), + ], + ); + + // expect: + expect(container.read(intProvider), 42); // <3> + expect(container.read(otherIntProvider), 42); + }); +} +``` -## Badges -On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge. + -## Visuals -Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method. +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>`. -## Installation -Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection. +## Changer d'état avec le StateProvider + +Le `Provider` est un moyen de récupérer un état, mais en aucun cas de le modifier. +Ce comportement a été confié au `StateProvider` de la manière suivante : + +[_source : state_provider.dart_](https://gitlab.ippon.fr/adbonnin/article_flutter_riverpod/-/blob/main/test/state_provider.dart) +```dart +final intProvider = StateProvider((_) => 13); // <1> + +void main() { + test('doit modifier un état', () { + // given: + final container = createContainer(); + + // expect: + expect(container.read(intProvider), equals(13)); + + // when: + container.read(intProvider.notifier).state = 42; // <2> + + // expect: + expect(container.read(intProvider), equals(42)); // <3> + }); +} +``` + + + +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>`. +L'état du `StateProvider` a bien été mis à jour avec cette nouvelle valeur `<3>`. + +> Le contrat du `StateProvider` va de la récupération à la modification d'un état. +> Les modifications sont réalisées en affectant un nouvel objet à la variable `state` du `notifier`. +> Chacune de ces modifications est ensuite notifiée aux objets qui le surveillent. + +Ce fonctionnement implique la création d'un nouvel objet pour indiquer un changement d'état et conforte l'utilisation d'objets immutables. + +La mise à jour de l'état par le `StateProvider` met en évidence un pattern récurrent chez Riverpod. +Étant donné que le `ProviderContainer` propose la méthode `read`, on aurait pu s'attendre qu'il propose son pendant, la méthode `write`. +D'un point de vue conceptuel, l'unique interaction que partagent toutes les classes de `Provider` avec le `ProviderContainer` est de récupérer un état, d'où l'unique présence de la méthode read. +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 + +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. + +[_source : watch.dart_](https://gitlab.ippon.fr/adbonnin/article_flutter_riverpod/-/blob/main/test/watch.dart) +```dart +final intProvider = StateProvider<int>((_) => 13); // <1> +final readProvider = Provider<int>((ref) => ref.read(intProvider)); +final watchProvider = Provider<int>((ref) => ref.watch(intProvider)); + +void main() { + test('doit surveiller un état', () { + // given: + final container = createContainer(); + + // expect: + expect(container.read(readProvider), equals(13)); // <2> + expect(container.read(watchProvider), equals(13)); + + // when: + container.read(intProvider.notifier).state = 42; // <3> + + // expect: + expect(container.read(readProvider), equals(13)); // <4> + expect(container.read(watchProvider), equals(42)); + }); +} +``` + + + +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>`. +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. +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`. + +Ce fonctionnement 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 +@override +T read<T>(ProviderBase<T> provider) { + _assertNotOutdated(); + assert(!_debugIsRunningSelector, 'Cannot call ref.read inside a selector'); + assert(_debugAssertCanDependOn(provider), ''); + return _container.read(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 : + +[_source : dependance_cyclique.dart_](https://gitlab.ippon.fr/adbonnin/article_flutter_riverpod/-/blob/main/test/dependance_cyclique.dart) +```dart +final Matcher throwsProviderException = throwsA(const TypeMatcher<ProviderException>()); + +final Provider<int> provider = Provider<int>((ref) => ref.watch(otherProvider)); // <1> +final Provider<int> otherProvider = Provider<int>((ref) => ref.watch(provider)); + +void main() { + test('doit lever une exception suite à une dépendance cyclique', () { + // given: + final container = createContainer(); + + // expect: + expect(() => container.read(provider), throwsProviderException); // <2> + }); +} +``` + +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. + +## Le listener qui écoutait à l'oreille des Providers + +Un autre moyen d'être notifié d'un changement d'état est de l'écouter avec la méthode `listen` proposée par le `ProviderContainer`. +Cette méthode prend en paramètre une callback qui sera appelée lors de chaque changement d'état en passant en paramètres l'ancienne et la nouvelle valeur. + +[_source : listener.dart_](https://gitlab.ippon.fr/adbonnin/article_flutter_riverpod/-/blob/main/test/listener.dart) +```dart +final intProvider = StateProvider<int>((_) => 13); // <1> +final watchProvider = Provider<int>((ref) => ref.watch(intProvider)); + +void main() { + test('doit écouter un état', () { + const defaultValue = -1; + + // given: + final container = createContainer(); + + var intValue = defaultValue; // <2> + container.listen<int>(intProvider, (_, next) => intValue = next); + + var watchValue = defaultValue; + container.listen<int>(watchProvider, (_, next) => watchValue = next); + + // expect: + expect(intValue, equals(defaultValue)); // <3> + expect(watchValue, equals(defaultValue)); + + // when: + container.read(intProvider.notifier).state = 42; // <4> + + // then: + expect(intValue, equals(42)); // <5> + expect(watchValue, equals(defaultValue)); + + // when: + container.read(watchProvider); // <6> + + // then: + expect(intValue, equals(42)); // <7> + expect(watchValue, equals(42)); + }); +} +``` + + + +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>`. +Sans aucune modification, ces valeurs écoutées conservent leurs valeurs par défaut `<3>`. +Après la modification de l'état du `intProvider` avec la valeur 42 `<4>` seulement son listener a été notifié `<5>`. +Ce n'est qu'après la lecture du `watchProvider` que son listener est notifié `<6>`. + +> Écouter n'est pas surveiller. + +É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>`. + +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__. + +> 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`. + +## Devenir sélectif dans les changements d'états + +Être notifié par tous les changements d'états peut mener à des pertes de performances. +Ce problème est résolu par la méthode `select` des `Provider` qui permet d'agréger leur état pour ne conserver que les valeurs utiles. + +[_source : select.dart_](https://gitlab.ippon.fr/adbonnin/article_flutter_riverpod/-/blob/main/test/select.dart) +```dart +final intProvider = StateProvider((_) => 13); // <1> +final moduloProvider = Provider<int>((ref) => ref.watch(intProvider.select((state) => state % 10))); + +void main() { + test('doit écouter le modulo 10', () { + // given: + final container = createContainer(); + + // and: + var called = 0; + container.listen(moduloProvider, (_, __) => called++); // <2> -## Usage -Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README. + // expect: + expect(container.read(moduloProvider), equals(3)); // <3> + expect(called, equals(0)); -## Support -Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc. + // when: + container.read(intProvider.notifier).state = 42; // <4> -## Roadmap -If you have ideas for releases in the future, it is a good idea to list them in the README. + // then: + expect(container.read(moduloProvider), equals(2)); // <5> + expect(called, equals(1)); -## Contributing -State if you are open to contributions and what your requirements are for accepting them. + // when: + container.read(intProvider.notifier).state = 22; // <6> + + // expect: + expect(container.read(moduloProvider), equals(2)); // <7> + expect(called, equals(1)); + }); +} +``` + + + +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>`. +Initialement le modulo de 13 vaut 3 et aucun changement n'est encore réalisé `<3>`. +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 : + +```dart +final moduloProvider = Provider<int>((ref) => ref.watch(intProvider) % 10); +``` + +Le `select` prendra tout son intérêt lors de l'intégration avec Flutter afin de ne conserver que les données utiles à surveiller pour économiser les rebuild et gagner en performances. + +## Accéder au notifier avec le StateNotifierProvider + +Jusqu'à présent les `Provider`s ne donnaient accès qu'à un état, qu'il soit non modifiable avec un `Provider` ou modifiable avec un `StateProvider` par l'intermédiaire de son `notifier`. +Le `StateNotifierProvider` donne accès à ce `notifier` pour permettre au développeur de l'enrichir avec de nouvelles méthodes : + +```dart +final incrementProvider = StateNotifierProvider<IncrementNotifier, int>( + (ref) => IncrementNotifier(ref.watch(intProvider)), +); + +class IncrementNotifier extends StateNotifier<int> { + IncrementNotifier(int value) : super(value); + + void increment() { + state++; + } +} +``` + +Le `StateNotifierProvider` est formé de deux composants : le __contenant__ avec la classe `StateNotifier` et le __contenu__ avec sa variable `state`. +Chacun dispose de son propre cycle de vie, celui de l'état étant dépendant de celui du notifier : + +[_source : state_notifier_provider.dart_](https://gitlab.ippon.fr/adbonnin/article_flutter_riverpod/-/blob/main/test/state_notifier_provider.dart) +```dart +final intProvider = StateProvider<int>((ref) => 13); // <1> + +final incrementProvider = StateNotifierProvider<IncrementNotifier, int>( + (ref) => IncrementNotifier(ref.watch(intProvider)), +); + +class IncrementNotifier extends StateNotifier<int> { + IncrementNotifier(int value) : super(value); + + void increment() { // <2> + state++; + } +} + +void main() { + test('doit incrémenter un état', () { + // given: + final container = createContainer(); + + // expect: + expect(container.read(incrementProvider), equals(13)); // <3> + + // when: + container.read(incrementProvider.notifier).increment(); // <4> + + // then: + expect(container.read(incrementProvider), equals(14)); // <5> + + // when: + container.read(intProvider.notifier).state = 42; // <6> + + // then: + expect(container.read(incrementProvider), equals(42)); // <7> + }); +} +``` + + + +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>`. +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. + +> 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. +> 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. +Le `ChangeNotifierProvider` répond à ce cas de figure en laissant à la charge du développeur de notifier les changements apportés à l'état : + +```dart +final incrementProvider = ChangeNotifierProvider<IncrementNotifier>( + (ref) => IncrementNotifier(13), +); + +class IncrementNotifier extends ChangeNotifier { + IncrementNotifier(this.value); + + int value; + + void increment() { + value++; // <1> Modification de l'état + notifyListeners(); // <2> Notification de l'état + } +} +``` + +Comme le `StateNotifierProvider`, il se compose d'un __contenant__ avec le `ChangeNotifier` mais cette fois-ci qui déclare lui-même son propre __contenu__. + +> Le contrat du `ChangeNotifierProvider` va de la récupération à la modification de l'état. +> La classe `ChangeNotifier` est à étendre et les changements internes sont à notifier manuellement en appelant la méthode `notifyListeners`. +> Les notifications sont ensuite transmises aux objets qui les surveillent. + +[_source : change_notifier_provider.dart_](https://gitlab.ippon.fr/adbonnin/article_flutter_riverpod/-/blob/main/test/change_notifier_provider.dart) +```dart +final incrementProvider = ChangeNotifierProvider<IncrementNotifier>( + (ref) => IncrementNotifier(13), // <1> +); + +class IncrementNotifier extends ChangeNotifier { + IncrementNotifier(this.number); + + int number; + + void increment() { + number++; + notifyListeners(); + } +} + +void main() { + test('doit incrémenter un état', () { + // given: + final container = createContainer(); + + // expect: + expect(container.read(incrementProvider.notifier).number, equals(13)); // <2> + + // when: + container.read(incrementProvider).increment(); // <3> + + // then: + expect(container.read(incrementProvider).number, equals(14)); // <4> + }); +} +``` + + + +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. +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. + +## 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`. + +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`. + +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). + +[_source : future_provider.dart_](https://gitlab.ippon.fr/adbonnin/article_flutter_riverpod/-/blob/main/test/future_provider.dart) +```dart +const duration = Duration(milliseconds: 100); + +final asyncIntProvider = FutureProvider<int>( // <1> + (ref) => Future.delayed(duration, () => 13), +); + +void main() { + test('doit consommer un futur', () async { + // given: + final container = createContainer(); + + // expect: + expect(container.read(asyncIntProvider), equals(const AsyncValue<int>.loading())); // <2> + + // when: + await Future.delayed(duration + const Duration(milliseconds: 50)); // <3> + + // then: + expect(container.read(asyncIntProvider), equals(const AsyncValue.data(13))); // <4> + }); +} +``` + + + +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>`. +Après avoir attendu 150ms `<3>`, l'état devient un `AsynData` avec pour valeur 13 `<4>`. + +## Le flux et le StreamProvider + +Le fonctionnement d'une `Stream` est similaire à celui d'un `Future`, à ceci près qu'elle peut retourner plusieurs valeurs durant son cycle de vie : + +[_source : stream_provider.dart_](https://gitlab.ippon.fr/adbonnin/article_flutter_riverpod/-/blob/main/test/stream_provider.dart) +```dart +const duration = Duration(milliseconds: 100); + +final asyncIntProvider = StreamProvider<int>( // <1> + (ref) async* { + await Future.delayed(duration); + yield 13; + + await Future.delayed(duration); + throw Error(); + }, +); + +void main() { + test('doit consommer une stream', () async { + // given: + final container = createContainer(); + + // expect: + expect(container.read(asyncIntProvider), equals(const AsyncValue<int>.loading())); // <2> + + // when: + await Future.delayed(duration + const Duration(milliseconds: 50)); // <3> + + // then: + expect(container.read(asyncIntProvider), equals(const AsyncValue.data(13))); // <4> + + // when: + await Future.delayed(duration); // <5> + + // then: + expect(container.read(asyncIntProvider), isInstanceOf<AsyncError>()); // <6> + }); +} +``` -For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self. + -You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser. +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>`. -## Authors and acknowledgment -Show your appreciation to those who have contributed to the project. +## Conclusion -## License -For open source projects, say how it is licensed. +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. -## Project status -If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers. +[^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 diff --git a/docs/images/change_notifier_provider.png b/docs/images/change_notifier_provider.png new file mode 100644 index 0000000000000000000000000000000000000000..aa8186e1d772a983f830ec17b2f7587938bbc882 Binary files /dev/null and b/docs/images/change_notifier_provider.png differ diff --git a/docs/images/declaration_provider.png b/docs/images/declaration_provider.png new file mode 100644 index 0000000000000000000000000000000000000000..9100e74f7499eab23195885bb14797f3e9789bc1 Binary files /dev/null and b/docs/images/declaration_provider.png differ diff --git a/docs/images/future_provider.png b/docs/images/future_provider.png new file mode 100644 index 0000000000000000000000000000000000000000..ee44d69a42723e4287a0bfde0ff9f0543621c0b9 Binary files /dev/null and b/docs/images/future_provider.png differ diff --git a/docs/images/listener.png b/docs/images/listener.png new file mode 100644 index 0000000000000000000000000000000000000000..0caf619a8af637099cb1717af965d00d8b3c07ea Binary files /dev/null and b/docs/images/listener.png differ diff --git a/docs/images/provider.png b/docs/images/provider.png new file mode 100644 index 0000000000000000000000000000000000000000..ea37a272df4647a9a3f37c9c3edda7dbaf0d4309 Binary files /dev/null and b/docs/images/provider.png differ diff --git a/docs/images/schemas.vsdx b/docs/images/schemas.vsdx new file mode 100644 index 0000000000000000000000000000000000000000..a799ef7d8bc7a7119c6cdfb33219d806720b966f Binary files /dev/null and b/docs/images/schemas.vsdx differ diff --git a/docs/images/select.png b/docs/images/select.png new file mode 100644 index 0000000000000000000000000000000000000000..32368d0d69c23fbafac4d8ed94f90dd6cea51ca5 Binary files /dev/null and b/docs/images/select.png differ diff --git a/docs/images/state_notifier_provider.png b/docs/images/state_notifier_provider.png new file mode 100644 index 0000000000000000000000000000000000000000..38e43f4ed761b2d77f69419c85f3cebf8b55de00 Binary files /dev/null and b/docs/images/state_notifier_provider.png differ diff --git a/docs/images/state_provider.png b/docs/images/state_provider.png new file mode 100644 index 0000000000000000000000000000000000000000..343dceef40cc6762ba82eaf7ae57dd3522510a01 Binary files /dev/null and b/docs/images/state_provider.png differ diff --git a/docs/images/stream_provider.png b/docs/images/stream_provider.png new file mode 100644 index 0000000000000000000000000000000000000000..2bf25bae7f3909ce0a859e60f846a9d810b71ef9 Binary files /dev/null and b/docs/images/stream_provider.png differ diff --git a/docs/images/test_overrides.png b/docs/images/test_overrides.png new file mode 100644 index 0000000000000000000000000000000000000000..a17557e24cbc9ee393f52389df4deebced02130c Binary files /dev/null and b/docs/images/test_overrides.png differ diff --git a/docs/images/watch.png b/docs/images/watch.png new file mode 100644 index 0000000000000000000000000000000000000000..7789c1f6910372873ed9b7626b54799ed1d9fc59 Binary files /dev/null and b/docs/images/watch.png differ diff --git a/test/change_notifier_provider.dart b/test/change_notifier_provider.dart new file mode 100644 index 0000000000000000000000000000000000000000..cc2fa95b67cbc3e7b74a7163c64c91a8853839d9 --- /dev/null +++ b/test/change_notifier_provider.dart @@ -0,0 +1,36 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'utils.dart'; + +final incrementProvider = ChangeNotifierProvider<IncrementNotifier>( + (ref) => IncrementNotifier(13), // <1> +); + +class IncrementNotifier extends ChangeNotifier { + IncrementNotifier(this.number); + + int number; + + void increment() { + number++; + notifyListeners(); + } +} + +void main() { + test('doit incrémenter un état', () { + // given: + final container = createContainer(); + + // expect: + expect(container.read(incrementProvider.notifier).number, equals(13)); // <2> + + // when: + container.read(incrementProvider).increment(); // <3> + + // then: + expect(container.read(incrementProvider).number, equals(14)); // <4> + }); +} diff --git a/test/dependance_cyclique.dart b/test/dependance_cyclique.dart new file mode 100644 index 0000000000000000000000000000000000000000..abe30b7fed73e5b080b4e512f17ea71f9645bf10 --- /dev/null +++ b/test/dependance_cyclique.dart @@ -0,0 +1,19 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'utils.dart'; + +final Matcher throwsProviderException = throwsA(const TypeMatcher<ProviderException>()); + +final Provider<int> provider = Provider<int>((ref) => ref.watch(otherProvider)); // <1> +final Provider<int> otherProvider = Provider<int>((ref) => ref.watch(provider)); + +void main() { + test('doit lever une exception suite à une dépendance cyclique', () { + // given: + final container = createContainer(); + + // expect: + expect(() => container.read(provider), throwsProviderException); // <2> + }); +} diff --git a/test/future_provider.dart b/test/future_provider.dart new file mode 100644 index 0000000000000000000000000000000000000000..5e81976fdf1c64b2353fb6b1f9fed7681d56c1c3 --- /dev/null +++ b/test/future_provider.dart @@ -0,0 +1,26 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'utils.dart'; + +const duration = Duration(milliseconds: 100); + +final asyncIntProvider = FutureProvider<int>( // <1> + (ref) => Future.delayed(duration, () => 13), +); + +void main() { + test('doit consommer un futur', () async { + // given: + final container = createContainer(); + + // expect: + expect(container.read(asyncIntProvider), equals(const AsyncValue<int>.loading())); // <2> + + // when: + await Future.delayed(duration + const Duration(milliseconds: 50)); // <3> + + // then: + expect(container.read(asyncIntProvider), equals(const AsyncValue.data(13))); // <4> + }); +} diff --git a/test/listener.dart b/test/listener.dart new file mode 100644 index 0000000000000000000000000000000000000000..934fd4f9744eba9ca4ece76d7bf5919d08d5c503 --- /dev/null +++ b/test/listener.dart @@ -0,0 +1,40 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'utils.dart'; + +final intProvider = StateProvider<int>((_) => 13); // <1> +final watchProvider = Provider<int>((ref) => ref.watch(intProvider)); + +void main() { + test('doit écouter un état', () { + const defaultValue = -1; + + // given: + final container = createContainer(); + + var intValue = defaultValue; // <2> + container.listen<int>(intProvider, (_, next) => intValue = next); + + var watchValue = defaultValue; + container.listen<int>(watchProvider, (_, next) => watchValue = next); + + // expect: + expect(intValue, equals(defaultValue)); // <3> + expect(watchValue, equals(defaultValue)); + + // when: + container.read(intProvider.notifier).state = 42; // <4> + + // then: + expect(intValue, equals(42)); // <5> + expect(watchValue, equals(defaultValue)); + + // when: + container.read(watchProvider); // <6> + + // then: + expect(intValue, equals(42)); // <7> + expect(watchValue, equals(42)); + }); +} diff --git a/test/provider.dart b/test/provider.dart new file mode 100644 index 0000000000000000000000000000000000000000..66bc6c65c6661900bf6c4b7a02994b185506e8df --- /dev/null +++ b/test/provider.dart @@ -0,0 +1,15 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; + +final intProvider = Provider<int>((_) => 13); // <1> + +void main() { + test('doit récupérer un état', () { + // given: + final container = ProviderContainer(); // <2> + addTearDown(container.dispose); + + // expect: + expect(container.read(intProvider), equals(13)); // <3> + }); +} diff --git a/test/select.dart b/test/select.dart new file mode 100644 index 0000000000000000000000000000000000000000..2fe9daed6c01ad2acd3c48fc477293005345f1e9 --- /dev/null +++ b/test/select.dart @@ -0,0 +1,36 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'utils.dart'; + +final intProvider = StateProvider((_) => 13); // <1> +final moduloProvider = Provider<int>((ref) => ref.watch(intProvider.select((state) => state % 10))); + +void main() { + test('doit écouter le modulo 10', () { + // given: + final container = createContainer(); + + // and: + var called = 0; + container.listen(moduloProvider, (_, __) => called++); // <2> + + // expect: + expect(container.read(moduloProvider), equals(3)); // <3> + expect(called, equals(0)); + + // when: + container.read(intProvider.notifier).state = 42; // <4> + + // then: + expect(container.read(moduloProvider), equals(2)); // <5> + expect(called, equals(1)); + + // when: + container.read(intProvider.notifier).state = 22; // <6> + + // expect: + expect(container.read(moduloProvider), equals(2)); // <7> + expect(called, equals(1)); + }); +} diff --git a/test/state_notifier_provider.dart b/test/state_notifier_provider.dart new file mode 100644 index 0000000000000000000000000000000000000000..b9445bd8e730b73dee279fc2859c0ec9e8ac5063 --- /dev/null +++ b/test/state_notifier_provider.dart @@ -0,0 +1,40 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import 'utils.dart'; + +final intProvider = StateProvider<int>((ref) => 13); // <1> + +final incrementProvider = StateNotifierProvider<IncrementNotifier, int>( + (ref) => IncrementNotifier(ref.watch(intProvider)), +); + +class IncrementNotifier extends StateNotifier<int> { + IncrementNotifier(int value) : super(value); + + void increment() { // <2> + state++; + } +} + +void main() { + test('doit incrémenter un état', () { + // given: + final container = createContainer(); + + // expect: + expect(container.read(incrementProvider), equals(13)); // <3> + + // when: + container.read(incrementProvider.notifier).increment(); // <4> + + // then: + expect(container.read(incrementProvider), equals(14)); // <5> + + // when: + container.read(intProvider.notifier).state = 42; // <6> + + // then: + expect(container.read(incrementProvider), equals(42)); // <7> + }); +} diff --git a/test/state_provider.dart b/test/state_provider.dart new file mode 100644 index 0000000000000000000000000000000000000000..edd0f886b61118740c21b660048b317641c2cc3c --- /dev/null +++ b/test/state_provider.dart @@ -0,0 +1,22 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'utils.dart'; + +final intProvider = StateProvider((_) => 13); // <1> + +void main() { + test('doit modifier un état', () { + // given: + final container = createContainer(); + + // expect: + expect(container.read(intProvider), equals(13)); + + // when: + container.read(intProvider.notifier).state = 42; // <2> + + // expect: + expect(container.read(intProvider), equals(42)); // <3> + }); +} diff --git a/test/stream_provider.dart b/test/stream_provider.dart new file mode 100644 index 0000000000000000000000000000000000000000..ad37191790f2832f046e3700cdc937b3448f3569 --- /dev/null +++ b/test/stream_provider.dart @@ -0,0 +1,38 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'utils.dart'; + +const duration = Duration(milliseconds: 100); + +final asyncIntProvider = StreamProvider<int>( // <1> + (ref) async* { + await Future.delayed(duration); + yield 13; + + await Future.delayed(duration); + throw Error(); + }, +); + +void main() { + test('doit consommer une stream', () async { + // given: + final container = createContainer(); + + // expect: + expect(container.read(asyncIntProvider), equals(const AsyncValue<int>.loading())); // <2> + + // when: + await Future.delayed(duration + const Duration(milliseconds: 50)); // <3> + + // then: + expect(container.read(asyncIntProvider), equals(const AsyncValue.data(13))); // <4> + + // when: + await Future.delayed(duration); // <5> + + // then: + expect(container.read(asyncIntProvider), isInstanceOf<AsyncError>()); // <6> + }); +} diff --git a/test/test_overrides.dart b/test/test_overrides.dart new file mode 100644 index 0000000000000000000000000000000000000000..0d1d66e0ac356eb22f26f5256e282d8ba3b22941 --- /dev/null +++ b/test/test_overrides.dart @@ -0,0 +1,23 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'utils.dart'; + +final intProvider = Provider<int>((_) => 13); // <1> +final otherIntProvider = Provider<int>((_) => 13); + +void main() { + test('doit surcharger le comportement des Provider', () { + // given: + final container = createContainer( + overrides: [ + intProvider.overrideWithProvider(Provider<int>((_) => 42)), // <2> + otherIntProvider.overrideWithValue(42), + ], + ); + + // expect: + expect(container.read(intProvider), 42); // <3> + expect(container.read(otherIntProvider), 42); + }); +} diff --git a/test/utils.dart b/test/utils.dart new file mode 100644 index 0000000000000000000000000000000000000000..0d35a8d62cf3e8881822e047fc4129e9f29a165e --- /dev/null +++ b/test/utils.dart @@ -0,0 +1,16 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; + +ProviderContainer createContainer({ + ProviderContainer? parent, + List<Override> overrides = const [], + List<ProviderObserver>? observers, +}) { + final container = ProviderContainer( + parent: parent, + overrides: overrides, + observers: observers, + ); + addTearDown(container.dispose); + return container; +} diff --git a/test/watch.dart b/test/watch.dart new file mode 100644 index 0000000000000000000000000000000000000000..876dfe860d127f0951821c2ffe597ae46f4a12ce --- /dev/null +++ b/test/watch.dart @@ -0,0 +1,22 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'utils.dart'; + +final intProvider = StateProvider((_) => 13); // <1> + +void main() { + test('doit surveiller un état', () { + // given: + final container = createContainer(); + + // expect: + expect(container.read(intProvider), equals(13)); + + // when: + container.read(intProvider.notifier).state = 42; // <2> + + // expect: + expect(container.read(intProvider), equals(42)); // <3> + }); +} diff --git a/test/widget_test.dart b/test/widget_test.dart deleted file mode 100644 index 35e5611d7391a71519e1303a643fcd6d7140c554..0000000000000000000000000000000000000000 --- a/test/widget_test.dart +++ /dev/null @@ -1,30 +0,0 @@ -// This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility in the flutter_test package. For example, you can send tap and scroll -// gestures. You can also use WidgetTester to find child widgets in the widget -// tree, read text, and verify that the values of widget properties are correct. - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'package:article_flutter_riverpod/main.dart'; - -void main() { - testWidgets('Counter increments smoke test', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(const MyApp()); - - // Verify that our counter starts at 0. - expect(find.text('0'), findsOneWidget); - expect(find.text('1'), findsNothing); - - // Tap the '+' icon and trigger a frame. - await tester.tap(find.byIcon(Icons.add)); - await tester.pump(); - - // Verify that our counter has incremented. - expect(find.text('0'), findsNothing); - expect(find.text('1'), findsOneWidget); - }); -}