diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000000000000000000000000000000000000..5152928dd41a382ec940e6c70f01b2a577ce1967 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,29 @@ +stages: + - test + - build + - deploy + +test:flutter: + stage: test + image: cirrusci/flutter + script: + - flutter test + +build:web-main: + stage: build + image: cirrusci/flutter + script: + - flutter build web --release --base-href '/article_flutter_riverpod/' + artifacts: + paths: + - build/web + +pages: + stage: deploy + script: + - cp -r build/web public + artifacts: + paths: + - public + only: + - main \ No newline at end of file diff --git a/README.md b/README.md index 828170c94b7c371dca773f2617fbf3553b0c1415..ce1ef8a18c5dea8bbac554858aeb33d4b69c529d 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,4 @@ # Riverpod par la pratique -* [Découverte des bases](docs/01_decouverte_des_bases.md) \ No newline at end of file +* [Découverte des bases](docs/01_decouverte_des_bases.md) +* [Intégration avec Flutter](docs/02_integration_avec_flutter.md) \ No newline at end of file diff --git a/docs/01_decouverte_des_bases.md b/docs/01_decouverte_des_bases.md index 99c984b452a154ba9b4f17bc3427390fbd66abdd..a91cc364988d2deefb407966b728c28a96c48f2b 100644 --- a/docs/01_decouverte_des_bases.md +++ b/docs/01_decouverte_des_bases.md @@ -1,6 +1,7 @@ # Riverpod par la pratique : découverte des bases Cet article a pour objectif de vous faire découvrir pas à pas la bibliothèque Riverpod à partir d'exemples d'utilisation. +Le code source est disponible sur le [GitLab Ippon](https://gitlab.ippon.fr/adbonnin/article_flutter_riverpod/-/tree/main/test/article_01). ## Il était une fois la gestion d’états @@ -21,7 +22,7 @@ Cet article fait référence à la bibliothèque Provider sous ces termes unique 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. +- [`flutter_riverpod`](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_hooks`](https://pub.dev/packages/flutter_hooks). La bibliothèque `flutter_riverpod` sera utilisée pour aborder l'ensemble des fondamentaux de Riverpod. diff --git a/docs/02_integration_avec_flutter.md b/docs/02_integration_avec_flutter.md new file mode 100644 index 0000000000000000000000000000000000000000..ff77cd5977b44c2a3592ecd056e1463c87786642 --- /dev/null +++ b/docs/02_integration_avec_flutter.md @@ -0,0 +1,331 @@ +# Riverpod par la pratique : Intégration avec Flutter + +Cet article à pour objectif de vous faire découvrir l'intégration de Riverpod dans une application Flutter. +Il fait suite au précédent article sur la découverte des bases et s'appuie pour sur ses notions pour en ajouter de nouvelles. +Le code source est disponible sur le [GitLab Ippon](https://gitlab.ippon.fr/adbonnin/article_flutter_riverpod/-/tree/main/lib/article_02) et les exemples sur la [GitLab page](http://adbonnin.pages-gitlab.ippon.fr/article_flutter_riverpod/#/) associée. + +## Intégration dans une application + +Dans Flutter, tout est widget. +En suivant ce principe, Riverpod s'adapte à Flutter avec le widget `ProviderScope` dont la responsabilité est de partager ses états aux widgets enfants. +Plusieurs manières permettent d'accéder à ces états, le widget `Consumer` étant la solution la moins intrusive : + +[_source : consumer_example.dart_](https://gitlab.ippon.fr/adbonnin/article_flutter_riverpod/-/blob/main/lib/article_02/consumer_example.dart) + +```dart +final helloProvider = Provider<String>((ref) => 'Hello world'); // <1> + +class IntegrationExample extends StatelessWidget { + const IntegrationExample({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return const ProviderScope( // <2> + child: MyExample(), + ); + } +} + +class MyExample extends StatelessWidget { + const MyExample({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Consumer( // <3> + builder: (_, WidgetRef ref, __) => Text( + ref.watch(helloProvider), // <4> + textDirection: TextDirection.ltr, + ), + ); + } +} +``` + +[](http://adbonnin.pages-gitlab.ippon.fr/article_flutter_riverpod/#/consumer) + +Un `Provider` est défini pour retourner `'Hello world'` `<1>`. +Les états sont disponibles pour les widgets enfants à partir du parent `ProviderScope` `<2>`. +La méthode `builder` du `Consumer` `<3>` fournit en paramètre un `WidgetRef` qui permet de surveiller le provider et afficher son état `<4>`. + +Le `ProviderScope` vient encapsuler dans un widget le `ProviderContainer` pour le rendre accessible aux widgets enfants par l'intermédiaire d'un `WidgetRef`. +De la même manière que le `ProviderRef`, le `WidgetRef` est une façade pour le `ProviderContainer` et le fonctionnement attendu des méthodes `read` et `watch` est identique. + +L'utilisation de la méthode `watch` indique que le widget sera reconstruit à chaque changement de l'état. +Étant donné que l'état du `Provider` `<1>` conservera toujours la même valeur, la méthode `read` aurait eu le même comportement que la méthode `watch` `<4>`. +Ce choix est motivé par la [documentation officielle](https://riverpod.dev/docs/concepts/reading/#using-refread-to-obtain-the-state-of-a-provider) qui favorise le fonctionnement réactif de la méthode `watch` par rapport à la méthode `read`. + +D'un point de vue conceptuel, à chaque fois qu'un widget utilise un `Provider` il en dévient dépendant. +Cette dépendance ne pose pas de problème sur le court terme mais peut s'avérer coûteuse sur le long terme. +A partir du moment où ces widgets sont utilisés il faut tenir compte de l'état des `Provider`s dont ils dépendent. +Cette préoccupation est particulièrement présente à l'écriture des tests où chaque `Provider` doit être correctement initialisé. + +Le widget `Consumer` est une manière simple d'accéder au `WidgetRef` mais vient alourdir l'écriture de la méthode `build` avec un widget supplémentaire. +Pour des cas plus complexes, Riverpod dispose de widgets spécifiques. + +## StatelessWidget et ConsumerWidget + +L'autre solution proposée par `Riverpod` pour accéder au `WidgetRef` est de remplacer à la déclaration la classe étendue. +La classe `ConsumerWidget` vient remplacer `StatelessWidget` pour les widgets sans états : + +[_source : stateless_example.dart_](https://gitlab.ippon.fr/adbonnin/article_flutter_riverpod/-/blob/main/lib/article_02/stateless_example.dart) + +```dart +final countServiceProvider = StateNotifierProvider<CountService, int>((ref) => CountService(42)); // <1> + +class CountService extends StateNotifier<int> { // <2> + CountService(int firstValue) : super(firstValue); + + void increment() { // <3> + state++; + } +} + +class StatelessExample extends StatelessWidget { + const StatelessExample({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return const ProviderScope( + child: Counter(), + ); + } +} + +class Counter extends ConsumerWidget { // <4> + const Counter({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context, WidgetRef ref) { // <5> + final currentValue = ref.watch(countServiceProvider); // <6> + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text("Current value: $currentValue"), // <7> + HSpace.m, + ElevatedButton( + onPressed: () => _onIncrement(ref), // <8> + child: const Text("Increment"), + ), + ], + ); + } + + void _onIncrement(WidgetRef ref) { + ref.read(countServiceProvider.notifier).increment(); // <9> + } +} +``` + +[](http://adbonnin.pages-gitlab.ippon.fr/article_flutter_riverpod/#/stateless) + +Un `StateNotifierProvider` est déclaré `<1>` pour le service `CountService` `<2>`. +Ce service est un `StateNotifier` dont le state est un nombre qui peut être incrémenté en appelant la méthode `increment` `<3>`. +La classe `ConsumerWidget` remplace `StatelessWidget` `<4>` et ajoute le paramètre `WidgetRef` au niveau de la méthode `build` `<5>`. +Le `WidgetRef` est utilisé de la même manière qu'avec le `Consumer`, les modifications de l'état sont écoutées `<6>` pour être affichées `<7>`. +Le même `WidgetRef` est ensuite passé en paramètre de la méthode appelée lors d'un clique sur le bouton `<8>`. +Lors de cette méthode, la lecture du `notifier` présent dans le `countServiceProvider` donne accés au service pour appeler la méthode `increment`. + +Remplacer la classe étendue par le `Widget` est une solution qui peut se réléver contraignante voir ridibitoire pour certains développeurs. +Cependant, ce choix d'implémentation simplifie l'intégration de Riverpod en donnant accès directement accès au `WidgetRef` depuis la méthode `build`. +Comme pour le `BuildContext`, ce `WidgetRef` est passé aux méthodes qui ont besoin d'accéder aux états. + +## StatefulWidget et ConsumerStatefulWidget + +Le `ConsumerWidget` venant remplacer le `StatelessWidget`, c'est tout naturellement que le `StatefulWidget` dispose de sa propre implémentation : + +[_source : stateful_example.dart_](https://gitlab.ippon.fr/adbonnin/article_flutter_riverpod/-/blob/main/lib/article_02/stateful_example.dart) + +```dart +final countServiceProvider = StateNotifierProvider<CountService, int>((ref) => CountService(42)); // <1> + +class CountService extends StateNotifier<int> { + CountService(int firstValue) : super(firstValue); + + void increment() { + state++; + } +} + +class StatefulExample extends StatelessWidget { + const StatefulExample({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return const ProviderScope( + child: Counter(), + ); + } +} + +class Counter extends ConsumerStatefulWidget { // <2> + const Counter({Key? key}) : super(key: key); + + @override + ConsumerState<Counter> createState() => _CounterState(); // <3> +} + +class _CounterState extends ConsumerState<Counter> { // <4> + late int firstValue; + + @override + void initState() { + super.initState(); + firstValue = ref.read(countServiceProvider); // <5> + } + + @override + Widget build(BuildContext context) { // <6> + final currentValue = ref.watch(countServiceProvider); // <7> + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text("First value: $firstValue"), // <8> + HSpace.s, + Text("Current value: $currentValue"), + HSpace.m, + ElevatedButton( + onPressed: _onIncrement, + child: const Text("Increment"), + ), + ], + ); + } + + void _onIncrement() { + ref.read(countServiceProvider.notifier).increment(); // <9> + } +} +``` + +[](http://adbonnin.pages-gitlab.ippon.fr/article_flutter_riverpod/#/stateful) + +Le service et le provider associé sont identiques à l'exemple précédent `<1>`. +Au niveau du widget, la classe `ConsumerStatefulWidget` remplace `StatefulWidget` `<2>`. +Au niveau du state, la classe `ConsumerState` remplace `ConsumerState` à la déclaration `<4>` et à la création `<3>`. +Contrairement au `ConsumerWidget`, la signature de la méthode `build` reste identique `<6>` car le `WidgetRef` est devenu une propriété. +La première valeur du service est lue à l'initialisation du widget `<5>` puis ses modifications sont écoutées `<7>` pour être affichées `<8>`. +Le `WidgetRef` en propriété est utilisé pour lire le service et incrémenter l'état. + +A la différence du `StatelessWidget`, le `WidgetRef` est directement accessible avec la propriété `ref`. +Ce choix d'implémentation donne accés aux états de n'importe où dans le widget en conservant un code lisible ; le `WidgetRef` n'étant plus passé de méthode en méthode. + +Jusqu'à présent, les `Provider`s étaient surveillés avec la méthode `watch`, dans le `builder` du `Consumer` ou dans la méthode `build` des widgets. +Dans les cas spécifiques où la surveillance est inutile le `Provider` est simplement lu avec la méthode `read`. +C'est par exemple le cas pour récupérer ponctuellement un état `<7>` ou appeler une fonction d'un `notifier` `<11>`. + +## Un lien de parenté + +Le widget `ProviderScope` est un conteneur qui peut avoir en descendance un autre `ProviderScope` pour former un lien de parenté : + +[_source : parent_example.dart_](https://gitlab.ippon.fr/adbonnin/article_flutter_riverpod/-/blob/main/lib/article_02/parent_example.dart) + +```dart +final countServiceProvider = StateNotifierProvider<CountService, int>((ref) => CountService(42)); // <1> + +class CountService extends StateNotifier<int> { + CountService(int firstValue) : super(firstValue); + + void increment() { + state++; + } +} + +final randomProvider = Provider<int>((ref) => Random().nextInt(10)); // <2> + +final multiplyByRandomProvider = Provider( // <3> + (ref) => ref.watch(countServiceProvider) * ref.watch(randomProvider), + dependencies: [ + countServiceProvider, + randomProvider, + ], +); + +class ParentExample extends StatelessWidget { + const ParentExample({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return ProviderScope( // <4> + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Counter(), // <5> + HSpace.xl, + ProviderScope( // <6> + overrides: [ + countServiceProvider.overrideWithValue(CountService(13)), // <7> + ], + child: const Counter(), // <8> + ), + ], + ), + ); + } +} + +class Counter extends ConsumerStatefulWidget { + const Counter({Key? key}) : super(key: key); + + @override + ConsumerState<Counter> createState() => _CounterState(); +} + +class _CounterState extends ConsumerState<Counter> { + late int firstValue; + + @override + void initState() { + super.initState(); + firstValue = ref.read(countServiceProvider); // <9> + } + + @override + Widget build(BuildContext context) { + final currentValue = ref.watch(countServiceProvider); // <10> + final random = ref.watch(randomProvider); + final multiplyByTwo = ref.watch(multiplyByRandomProvider); + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text("First value: $firstValue"), + HSpace.s, + Text("Current value: $currentValue"), + HSpace.s, + Text("Multiplied by $random: $multiplyByTwo"), + HSpace.m, + ElevatedButton( + onPressed: _onIncrement, + child: const Text("Increment"), + ) + ], + ); + } + + void _onIncrement() { + ref.read(countServiceProvider.notifier).increment(); // <11> + } +} +``` + +[](http://adbonnin.pages-gitlab.ippon.fr/article_flutter_riverpod/#/parent-providerscope) + +Le service et le provider associé sont identiques à l'exemple précédent `<1>`. +Un chiffre aléatoire est retourné par le `Provider` `randomProvider` `<2>`. +Le `Provider` `multiplyByRandomProvider` `<3>` vient écouter le provider `countServiceProvider` et retourne sa valeur multipliée le `randomProvider`. +Un premier `ProviderScope` est déclaré `<4>` avec un `Counter` associé `<5>`. +Un second `ProviderScope` est déclaré `<6>` en temps que widget enfant du précédent avec également un `Counter` associé `<8>`. +Ce second `ProviderScope` vient surcharger la valeur du `counterServiceProvider` `<7>`. +Comme précédemment, le widget `Counter` vient lire `<9>`, surveiller `<10>` et incrémenter `<11>` le `counterServiceProvider`. + +Les deux widgets `Counter`s partagent le même état du `Provider` `randomProvider` `<2>`. +Ce n'est qu'à partir du moment où le `ProviderScope` surcharge le `countServiceProvider` `<7>` que chacun dispose de sa propre valeur. +Le provider `multiplyByRandomProvider` dépendant de `countServiceProvider` dispose également de son propre état. +Cette dépendance a été indiqué lors de la déclaration du `multiplyByRandomProvider` `<3>` pour que Riverpod en ai conscience. +Il en résulte deux `Counter` qui interagissent avec des états indépendants avec un multiplicateur commun. + +## L'aventure continue + +Maintenant que toutes les bases sont posées vous devriez être en mesure d'utiliser Riverpod dans votre application. diff --git a/docs/images/cependant.gif b/docs/images/cependant.gif new file mode 100644 index 0000000000000000000000000000000000000000..087182b075c631eab8f525afcb24caa6e0b08bde Binary files /dev/null and b/docs/images/cependant.gif differ diff --git a/docs/images/consumer.png b/docs/images/consumer.png new file mode 100644 index 0000000000000000000000000000000000000000..1a8799ded8d2c14c71c12fe21a11622a3c389f6d Binary files /dev/null and b/docs/images/consumer.png differ diff --git a/docs/images/parent_provider_scope.png b/docs/images/parent_provider_scope.png new file mode 100644 index 0000000000000000000000000000000000000000..4ea571cbef4cf670dfcf8a2214cb5655ee74c1d5 Binary files /dev/null and b/docs/images/parent_provider_scope.png differ diff --git a/docs/images/stateful.png b/docs/images/stateful.png new file mode 100644 index 0000000000000000000000000000000000000000..93502060d5dad4bb6169f782dcde0a2a95f99d7a Binary files /dev/null and b/docs/images/stateful.png differ diff --git a/docs/images/stateless.png b/docs/images/stateless.png new file mode 100644 index 0000000000000000000000000000000000000000..e878f8ece1cd143919f972af7d1da0451941da20 Binary files /dev/null and b/docs/images/stateless.png differ diff --git a/lib/article_02/consumer_example.dart b/lib/article_02/consumer_example.dart new file mode 100644 index 0000000000000000000000000000000000000000..a161eca296a1e44cf56869242d223bf772eefe15 --- /dev/null +++ b/lib/article_02/consumer_example.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +final helloProvider = Provider<String>((ref) => 'Hello world'); // <1> + +class ConsumerExample extends StatelessWidget { + const ConsumerExample({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return const ProviderScope( // <2> + child: MyExample(), + ); + } +} + +class MyExample extends StatelessWidget { + const MyExample({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Consumer( // <3> + builder: (_, WidgetRef ref, __) => Text( + ref.watch(helloProvider), // <4> + textDirection: TextDirection.ltr, + ), + ); + } +} diff --git a/lib/article_02/parent_example.dart b/lib/article_02/parent_example.dart new file mode 100644 index 0000000000000000000000000000000000000000..19b327450c0b8c7b863acad29cad05a82a74a278 --- /dev/null +++ b/lib/article_02/parent_example.dart @@ -0,0 +1,92 @@ +import 'dart:math'; + +import 'package:article_flutter_riverpod/widgets/space.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +final countServiceProvider = StateNotifierProvider<CountService, int>((ref) => CountService(42)); // <1> + +class CountService extends StateNotifier<int> { + CountService(int firstValue) : super(firstValue); + + void increment() { + state++; + } +} + +final randomProvider = Provider<int>((ref) => Random().nextInt(10)); // <2> + +final multiplyByRandomProvider = Provider( // <3> + (ref) => ref.watch(countServiceProvider) * ref.watch(randomProvider), + dependencies: [ + countServiceProvider, + randomProvider, + ], +); + +class ParentExample extends StatelessWidget { + const ParentExample({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return ProviderScope( // <4> + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Counter(), // <5> + HSpace.xl, + ProviderScope( // <6> + overrides: [ + countServiceProvider.overrideWithValue(CountService(13)), // <7> + ], + child: const Counter(), // <8> + ), + ], + ), + ); + } +} + +class Counter extends ConsumerStatefulWidget { + const Counter({Key? key}) : super(key: key); + + @override + ConsumerState<Counter> createState() => _CounterState(); +} + +class _CounterState extends ConsumerState<Counter> { + late int firstValue; + + @override + void initState() { + super.initState(); + firstValue = ref.read(countServiceProvider); // <9> + } + + @override + Widget build(BuildContext context) { + final currentValue = ref.watch(countServiceProvider); // <10> + final random = ref.watch(randomProvider); + final multiplyByRandom = ref.watch(multiplyByRandomProvider); + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text("First value: $firstValue"), + HSpace.s, + Text("Current value: $currentValue"), + HSpace.s, + Text("Multiplied by $random: $multiplyByRandom"), + HSpace.m, + ElevatedButton( + onPressed: _onIncrement, + child: const Text("Increment"), + ) + ], + ); + } + + void _onIncrement() { + ref.read(countServiceProvider.notifier).increment(); // <11> + } +} diff --git a/lib/article_02/stateful_example.dart b/lib/article_02/stateful_example.dart new file mode 100644 index 0000000000000000000000000000000000000000..0f7a917f994f3f0489f0f92de76f54c31f3f0957 --- /dev/null +++ b/lib/article_02/stateful_example.dart @@ -0,0 +1,64 @@ +import 'package:article_flutter_riverpod/widgets/space.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +final countServiceProvider = StateNotifierProvider<CountService, int>((ref) => CountService(42)); // <1> + +class CountService extends StateNotifier<int> { + CountService(int firstValue) : super(firstValue); + + void increment() { + state++; + } +} + +class StatefulExample extends StatelessWidget { + const StatefulExample({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return const ProviderScope( + child: Counter(), + ); + } +} + +class Counter extends ConsumerStatefulWidget { // <2> + const Counter({Key? key}) : super(key: key); + + @override + ConsumerState<Counter> createState() => _CounterState(); // <3> +} + +class _CounterState extends ConsumerState<Counter> { // <4> + late int firstValue; + + @override + void initState() { + super.initState(); + firstValue = ref.read(countServiceProvider); // <5> + } + + @override + Widget build(BuildContext context) { // <6> + final currentValue = ref.watch(countServiceProvider); // <7> + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text("First value: $firstValue"), // <8> + HSpace.s, + Text("Current value: $currentValue"), + HSpace.m, + ElevatedButton( + onPressed: _onIncrement, + child: const Text("Increment"), + ), + ], + ); + } + + void _onIncrement() { + ref.read(countServiceProvider.notifier).increment(); // <9> + } +} diff --git a/lib/article_02/stateless_example.dart b/lib/article_02/stateless_example.dart new file mode 100644 index 0000000000000000000000000000000000000000..01208e87853d84096bb07e57a7c91583f3498ceb --- /dev/null +++ b/lib/article_02/stateless_example.dart @@ -0,0 +1,49 @@ +import 'package:article_flutter_riverpod/widgets/space.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +final countServiceProvider = StateNotifierProvider<CountService, int>((ref) => CountService(42)); // <1> + +class CountService extends StateNotifier<int> { // <2> + CountService(int firstValue) : super(firstValue); + + void increment() { // <3> + state++; + } +} + +class StatelessExample extends StatelessWidget { + const StatelessExample({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return const ProviderScope( + child: Counter(), + ); + } +} + +class Counter extends ConsumerWidget { // <4> + const Counter({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context, WidgetRef ref) { // <5> + final currentValue = ref.watch(countServiceProvider); // <6> + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text("Current value: $currentValue"), // <7> + HSpace.m, + ElevatedButton( + onPressed: () => _onIncrement(ref), // <8> + child: const Text("Increment"), + ), + ], + ); + } + + void _onIncrement(WidgetRef ref) { + ref.read(countServiceProvider.notifier).increment(); // <9> + } +} diff --git a/lib/example.dart b/lib/example.dart new file mode 100644 index 0000000000000000000000000000000000000000..85c4d23031e0400fd1614e15e57095e8bbe2c98c --- /dev/null +++ b/lib/example.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; + +class Example { + const Example({ + required this.title, + required this.icon, + required this.path, + required this.builder, + }); + + final String title; + final IconData icon; + final String path; + final WidgetBuilder builder; +} + +class ExampleContainer extends StatelessWidget { + const ExampleContainer( + this.example, { + Key? key, + }) : super(key: key); + + final Example example; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(example.title), + ), + body: Center( + child: SingleChildScrollView( + child: example.builder(context), + ), + ), + ); + } +} diff --git a/lib/main.dart b/lib/main.dart index 202509be15d8afab8cc0096ea1b9610bd3832dae..d4a19fef476ef41e4a700568cd16cd03b44d7bdd 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,115 +1,100 @@ +import 'package:article_flutter_riverpod/example.dart'; +import 'package:article_flutter_riverpod/article_02/consumer_example.dart'; +import 'package:article_flutter_riverpod/article_02/parent_example.dart'; +import 'package:article_flutter_riverpod/article_02/stateful_example.dart'; +import 'package:article_flutter_riverpod/article_02/stateless_example.dart'; import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +final examples = [ + Example( + title: 'Consumer', + icon: Icons.eco, + path: 'consumer', + builder: (_) => const ConsumerExample(), + ), + Example( + title: 'Stateless', + icon: Icons.remove_circle_outline, + path: 'stateless', + builder: (_) => const StatelessExample(), + ), + Example( + title: 'Stateful', + icon: Icons.add_circle_outline, + path: 'stateful', + builder: (_) => const StatefulExample(), + ), + Example( + title: 'Parent ProviderScope', + icon: Icons.child_care, + path: 'parent-providerscope', + builder: (_) => const ParentExample(), + ), +]; void main() { - runApp(const MyApp()); + runApp(ArticleApp()); } -class MyApp extends StatelessWidget { - const MyApp({Key? key}) : super(key: key); +class ArticleApp extends StatelessWidget { + ArticleApp({Key? key}) : super(key: key); + + final _router = GoRouter(routes: [ + GoRoute( + path: '/', + builder: (_, __) => const HomePage(), + routes: [ + for (Example example in examples) // + GoRoute( + path: example.path, + builder: (_, __) => ExampleContainer(example), + ) + ], + ), + ]); - // This widget is the root of your application. @override Widget build(BuildContext context) { - return MaterialApp( - title: 'Flutter Demo', + return MaterialApp.router( + routeInformationProvider: _router.routeInformationProvider, + routeInformationParser: _router.routeInformationParser, + routerDelegate: _router.routerDelegate, + title: 'Flutter par la pratique', theme: ThemeData( - // This is the theme of your application. - // - // Try running your application with "flutter run". You'll see the - // application has a blue toolbar. Then, without quitting the app, try - // changing the primarySwatch below to Colors.green and then invoke - // "hot reload" (press "r" in the console where you ran "flutter run", - // or simply save your changes to "hot reload" in a Flutter IDE). - // Notice that the counter didn't reset back to zero; the application - // is not restarted. primarySwatch: Colors.blue, ), - home: const MyHomePage(title: 'Flutter Demo Home Page'), ); } } -class MyHomePage extends StatefulWidget { - const MyHomePage({Key? key, required this.title}) : super(key: key); - - // This widget is the home page of your application. It is stateful, meaning - // that it has a State object (defined below) that contains fields that affect - // how it looks. - - // This class is the configuration for the state. It holds the values (in this - // case the title) provided by the parent (in this case the App widget) and - // used by the build method of the State. Fields in a Widget subclass are - // always marked "final". - - final String title; - - @override - State<MyHomePage> createState() => _MyHomePageState(); -} - -class _MyHomePageState extends State<MyHomePage> { - int _counter = 0; - - void _incrementCounter() { - setState(() { - // This call to setState tells the Flutter framework that something has - // changed in this State, which causes it to rerun the build method below - // so that the display can reflect the updated values. If we changed - // _counter without calling setState(), then the build method would not be - // called again, and so nothing would appear to happen. - _counter++; - }); - } +class HomePage extends StatelessWidget { + const HomePage({Key? key}) : super(key: key); @override Widget build(BuildContext context) { - // This method is rerun every time setState is called, for instance as done - // by the _incrementCounter method above. - // - // The Flutter framework has been optimized to make rerunning build methods - // fast, so that you can just rebuild anything that needs updating rather - // than having to individually change instances of widgets. return Scaffold( appBar: AppBar( - // Here we take the value from the MyHomePage object that was created by - // the App.build method, and use it to set our appbar title. - title: Text(widget.title), + title: const Text('Flutter par la pratique'), + ), + body: ListView.builder( + itemBuilder: _buildItem, + itemCount: examples.length, ), - body: Center( - // Center is a layout widget. It takes a single child and positions it - // in the middle of the parent. - child: Column( - // Column is also a layout widget. It takes a list of children and - // arranges them vertically. By default, it sizes itself to fit its - // children horizontally, and tries to be as tall as its parent. - // - // Invoke "debug painting" (press "p" in the console, choose the - // "Toggle Debug Paint" action from the Flutter Inspector in Android - // Studio, or the "Toggle Debug Paint" command in Visual Studio Code) - // to see the wireframe for each widget. - // - // Column has various properties to control how it sizes itself and - // how it positions its children. Here we use mainAxisAlignment to - // center the children vertically; the main axis here is the vertical - // axis because Columns are vertical (the cross axis would be - // horizontal). - mainAxisAlignment: MainAxisAlignment.center, - children: <Widget>[ - const Text( - 'You have pushed the button this many times:', - ), - Text( - '$_counter', - style: Theme.of(context).textTheme.headline4, - ), - ], - ), + ); + } + + Widget _buildItem(BuildContext context, int index) { + final example = examples[index]; + + return ListTile( + leading: Icon( + example.icon, + color: Theme.of(context).primaryColor, ), - floatingActionButton: FloatingActionButton( - onPressed: _incrementCounter, - tooltip: 'Increment', - child: const Icon(Icons.add), - ), // This trailing comma makes auto-formatting nicer for build methods. + title: Text(example.title), + trailing: const Icon(Icons.chevron_right), + onTap: () => context.push('/${example.path}'), ); } } diff --git a/lib/styles.dart b/lib/styles.dart new file mode 100644 index 0000000000000000000000000000000000000000..5b02ea4bbabe8507af9a7403309f74d5af9cf8e3 --- /dev/null +++ b/lib/styles.dart @@ -0,0 +1,6 @@ +class Insets { + static const double s = 4; + static const double m = 8; + static const double l = 16; + static const double xl = 32; +} diff --git a/lib/widgets/space.dart b/lib/widgets/space.dart new file mode 100644 index 0000000000000000000000000000000000000000..cb311a05073346bf9c57929c8df5e0e1d8a00f0f --- /dev/null +++ b/lib/widgets/space.dart @@ -0,0 +1,18 @@ +import 'package:article_flutter_riverpod/styles.dart'; +import 'package:flutter/material.dart'; + +class HSpace extends StatelessWidget { + const HSpace(this.size, {Key? key}) : super(key: key); + + static const s = HSpace(Insets.s); + static const m = HSpace(Insets.m); + static const l = HSpace(Insets.l); + static const xl = HSpace(Insets.xl); + + final double size; + + @override + Widget build(BuildContext context) { + return SizedBox(height: size); + } +} diff --git a/pubspec.lock b/pubspec.lock index 26b1c4186a7b9ac2016c763aa5f7b9bf33a0ff75..34870fccd56a3161c123d0c070a4b018cc3a2377 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -81,6 +81,25 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + go_router: + dependency: "direct main" + description: + name: go_router + url: "https://pub.dartlang.org" + source: hosted + version: "4.3.0" + js: + dependency: transitive + description: + name: js + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.4" lints: dependency: transitive description: @@ -88,6 +107,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.0" + logging: + dependency: transitive + description: + name: logging + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" matcher: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 3e2cba0fa566bf04e0469ca5da0ccd9d5d38e558..83a50ea3bffd6808c4813209dc38f83b2f9aa86f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -35,6 +35,7 @@ dependencies: # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.2 flutter_riverpod: ^1.0.3 + go_router: ^4.3.0 dev_dependencies: flutter_test: