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 sur ces notions pour en ajouter de nouvelles.
Le code source est disponible sur le GitLab Ippon et les exemples sur la GitLab page 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 les états qu'il contient à ses widgets enfants.
Plusieurs manières permettent d'accéder à ces états, le widget Consumer
étant la solution la moins intrusive :
source : 01_consumer_example.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,
),
);
}
}
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
permettant 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 néanmois motivé par la documentation officielle 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.
À 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 : 02_stateless_example.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>
}
}
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évéler contraignante voire rédhibitoire 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 à passer 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 : 03_stateful_example.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>
}
}
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.
À 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 <5>
ou appeler une fonction d'un notifier
<9>
.
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 : 04_parent_example.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>
}
}
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 par celle du randomProvider
.
Un premier ProviderScope
est déclaré <4>
avec un Counter
associé <5>
.
Un second ProviderScope
est déclaré <6>
en tant 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 ait 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 de comprendre et d'utiliser Riverpod dans vos applications.