Riverpod pour les utilisateurs de Provider
Cet article est destiné aux personnes qui connaissent le package Provider et qui souhaitent ou veulent se familiariser avec Riverpod.
La relation entre Riverpod et Provider
Riverpod est conçu pour être le successeur spirituel de Provider. D'où le nom "Riverpod", qui est une anagramme de "Provider".
Riverpod est né de la recherche de solutions aux diverses limitations techniques auxquelles Provider est confronté. À l'origine, Riverpod devait être une version majeure de Provider afin de résoudre ce problème. Mais il a été décidé de ne pas le faire, car il s'agirait d'un changement de rupture relativement important, et Provider est l'un des packages Flutter les plus utilisés.
Pourtant, sur le plan conceptuel, Riverpod et Provider sont assez similaires.
Les deux paquets remplissent un rôle similaire. Ils essaient tous deux de :
- de mettre en cache et de libérer de certains objets à état
- offrir un moyen de simuler ces objets pendant les tests
- offrir un moyen pour les Widgets d'écouter ces objets d'une manière simple.
En temps, pensez à Riverpod comme ce que Provider aurait pu être si s'il avait continué à mûrir pendant quelques années.
Riverpod corrige divers problèmes fondamentaux de Provider, tels que, mais sans s'y limiter :
- Simplifier considérablement la combinaison des "providers".
Au lieu d'un
ProxyProvider
fastidieux et sujet à des erreurs, Riverpod expose des utilitaires simples mais puissants tels que ref.watch et ref.listen. - Permettre à plusieurs "providers" d'exposer une valeur du même type.
Cela supprime la nécessité de définir des classes personnalisées alors que l'exposition d'une valeur simple, de typeint
ouString
fonctionnerait tout aussi bien. - Suppression de la nécessité de redéfinir les providers dans les tests. Avec Riverpod, les providers sont prêts à être utilisés dans les tests par défaut.
- Réduction de la dépendance excessive sur le "scoping" pour libérer des objets en offrant d'autres moyens de libérer des objets (autoDispose). Bien que puissant, le scoping d'un provider est assez avancé et difficile à mettre en place.
... Et plus encore.
Le seul véritable inconvénient de Riverpod est qu'il nécessite de changer le type de widget pour fonctionner :
- Au lieu d'étendre
StatelessWidget
, avec Riverpod vous devriez étendreConsumerWidget
. - Au lieu d'étendre
StatefulWidget
, avec Riverpod vous devriez étendreConsumerStatefulWidget
.
Mais ce désagrément est assez mineur dans le grand schéma des choses. Et cette exigence pourrait disparaître un jour.
Donc, pour répondre à la question que vous vous posez probablement:
Dois-je utiliser Provider ou Riverpod ?
Vous devriez probablement utiliser Riverpod.
Riverpod est beaucoup mieux conçu et pourrait conduire à des simplifications drastiques
de votre logique.
La difference entre Provider et Riverpod
Définition des providers
La principale différence entre les deux paquets est la façon dont les "providers" sont définis.
Avec Provider, les providers sont des widgets et sont placés en tant que tels dans l'arbre des widgets,
typiquement dans un MultiProvider
:
class Counter extends ChangeNotifier {
...
}
void main() {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider<Counter>(create: (context) => Counter()),
],
child: MyApp(),
)
);
}
Avec Riverpod, les providers ne sont pas des widgets. Ce sont plutôt des objets Dart ordinaires.
De même, les providers sont définis en dehors de l'arbre des widgets et sont déclarés comme des
variables finales globales.
De plus, pour que Riverpod fonctionne, il est nécessaire d'ajouter un widget ProviderScope
au-dessus
de l'application entière.
Ainsi, l'équivalent de l'exemple de Provider utilisant Riverpod serait :
// Les providers sont désormais des variables de premier niveau
final counterProvider = ChangeNotifierProvider<Counter>((ref) => Counter());
void main() {
runApp(
// Ce widget active Riverpod pour l'ensemble du projet.
ProviderScope(
child: MyApp(),
),
);
}
Remarquez que la définition du provider a simplement été déplacée de quelques lignes.
Étant donné que les providers de Riverpod sont de simples objets Dart, il est possible d'utiliser
Riverpod sans Flutter.
Par exemple, Riverpod peut être utilisé pour écrire des applications en ligne de commande.
Lecture de providers: BuildContext
Avec Provider, une façon de lire les providers est d'utiliser le BuildContext
d'un Widget.
Par exemple, si un provider a été défini comme :
Provider<Model>(...);
puis sa lecture en utilisant Provider est effectuée avec :
class Example extends StatelessWidget {
Widget build(BuildContext context) {
Model model = context.watch<Model>();
}
}
L'équivalent en Riverpod serait :
final modelProvider = Provider<Model>(...);
class Example extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
Model model = ref.watch(modelProvider);
}
}
Remarquez comment :
L'extrait Riverpod étend
ConsumerWidget
au lieu deStatelessWidget
. Ce différent type de widget ajoute un paramètre supplémentaire à notre fonctionbuild
:WidgetRef
.Au lieu de
BuildContext.watch
, dans Riverpod nous faisonsWidgetRef.watch
, en utilisant leWidgetRef
que nous avons obtenu deConsumerWidget
.Riverpod ne s'appuie pas sur les types génériques. Il s'appuie plutôt sur la variable créée à l'aide de la définition du provider.
Remarquez également la similitude des définitions. Provider et Riverpod utilisent tous deux le mot-clé "watch" pour décrire "ce widget doit se reconstruire lorsque la valeur change".
Riverpod utilise la même terminologie que Provider pour la lecture des providers.
BuildContext.watch
->WidgetRef.watch
BuildContext.read
->WidgetRef.read
Les règles pour context.watch
et context.read
s'appliquent aussi à Riverpod :
Dans la méthode build
, utilisez "watch". Dans les gestionnaires de clics et autres événements,
utilisez "read".
Lecture de providers: Consumer
Provider est livré en option avec un widget nommé Consumer
(et des variantes telles que Consumer2
)
pour lire les providers.
Consumer
est utile pour optimiser les performances, en permettant des reconstructions plus granulaires
de l'arbre des widgets - en ne mettant à jour que les widgets pertinents lorsque l'état change :
Ainsi, si un provider a été défini comme :
Provider<Model>(...);
Provider permet de lire ce provider en utilisant Consumer
avec :
Consumer<Model>(
builder: (BuildContext context, Model model, Widget? child) {
}
)
Riverpod a le même principe. Riverpod, aussi, a un widget nommé Consumer
.
pour exactement le même but.
Si nous avons défini un provider comme :
final modelProvider = Provider<Model>(...);
Puis en utilisant Consumer
nous pourrions faire :
Consumer(
builder: (BuildContext context, WidgetRef ref, Widget? child) {
Model model = ref.watch(modelProvider);
}
)
Remarquez comment Consumer
nous donne un objet WidgetRef
. C'est le même objet
que nous avons vu dans la partie précédente concernant ConsumerWidget
.
Combinaison de providers: ProxyProvider avec des objets stateless (sans état)
Lorsque vous utilisez un provider, la manière officielle de combiner des providers.
est d'utiliser le widget ProxyProvider
(ou des variantes telles que ProxyProvider2
).
Par exemple, nous pouvons définir :
class UserIdNotifier extends ChangeNotifier {
String? userId;
}
// ...
ChangeNotifierProvider<UserIdNotifier>(create: (context) => UserIdNotifier()),
A partir de là, nous avons deux options. Nous pouvons combiner UserIdNotifier
afin de créer un nouveau
provider "stateless" (typiquement une valeur immuable qui peut être surchargée ==).
Par exemple :
ProxyProvider<UserIdNotifier, String>(
update: (context, userIdNotifier, _) {
return 'L\'identifiant de cet utilisateur est ${userIdNotifier.userId}';
}
)
Ce provider retournera automatiquement un nouveau String
à chaque fois que
UserIdNotifier.userId
change.
Nous pouvons faire quelque chose de similaire dans Riverpod, mais la syntaxe est différente.
D'abord, dans Riverpod, la définition de notre UserIdNotifier
serait :
class UserIdNotifier extends ChangeNotifier {
String? userId;
}
// ...
final userIdNotifierProvider = ChangeNotifierProvider<UserIdNotifier>(
(ref) => UserIdNotifier(),
),
De là, pour générer notre String
basé sur le userId
, nous pourrions faire :
final labelProvider = Provider<String>((ref) {
UserIdNotifier userIdNotifier = ref.watch(userIdNotifierProvider);
return 'L\'identifiant de cet utilisateur est ${userIdNotifier.userId}';
});
Remarquez la ligne qui fait ref.watch(userIdNotifierProvider)
.
Cette ligne de code indique à Riverpod d'obtenir le contenu du userIdNotifierProvider
et que chaque fois que cette valeur change, labelProvider
sera recalculé aussi.
Ainsi, le String
émis par notre labelProvider
sera automatiquement mis à jour
chaque fois que le userId
change.
Cette ligne ref.watch
devrait être similaire. Ce modèle a été couvert précédemment
en expliquant comment lire les providers dans les widgets.
En effet, les providers sont maintenant capables d'écouter d'autres providers de la même façon
que les widgets.
Combinaison de providers: ProxyProvider avec des objets stateful (avec état)
Lorsque vous combinez des providers, une autre alternative consiste à exposer les
objets à état, comme une instance de ChangeNotifier
.
Pour cela, on peut utiliser ChangeNotifierProxyProvider
(ou des variantes comme ChangeNotifierProxyProvider2
).
Par exemple, nous pouvons définir :
class UserIdNotifier extends ChangeNotifier {
String? userId;
}
// ...
ChangeNotifierProvider<UserIdNotifier>(create: (context) => UserIdNotifier()),
Ensuite, nous pouvons définir un nouveau ChangeNotifier
qui est basé sur UserIdNotifier.userId
.
Par exemple, nous pourrions faire :
class UserNotifier extends ChangeNotifier {
String? _userId;
void setUserId(String? userId) {
if (userId != _userId) {
print('The user ID changed from $_userId to $userId');
_userId = userId;
}
}
}
// ...
ChangeNotifierProxyProvider<UserIdNotifier, UserNotifier>(
create: (context) => UserNotifier(),
update: (context, userIdNotifier, userNotifier) {
return userNotifier!
..setUserId(userIdNotifier.userId);
},
);
Ce nouveau provider crée une seule instance de UserNotifier
(qui n'est jamais reconstruite)
et imprime une chaîne à chaque fois que l'ID de l'utilisateur change.
Faire la même chose en provider est réalisé différemment.
D'abord, dans Riverpod, la définition de notre UserIdNotifier
serait :
class UserIdNotifier extends ChangeNotifier {
String? userId;
}
// ...
final userIdNotifierProvider = ChangeNotifierProvider<UserIdNotifier>(
(ref) => UserIdNotifier(),
),
A partir de là, l'équivalent du précédent ChangeNotifierProxyProvider
serait :
class UserNotifier extends ChangeNotifier {}
final userNotfierProvider = ChangeNotifierProvider<UserNotifier>((ref) {
final userNotifier = UserNotifier();
ref.listen<UserIdNotifier>(
userIdNotifierProvider,
(previous, next) {
if (previous?.userId != next.userId) {
print('L\'ID utilisateur est passé de ${previous?.userId} à ${next.userId}');
}
},
);
return userNotifier;
});
Le coeur de cet extrait est la ligne ref.listen
.
Cette fonction ref.listen
est un utilitaire qui permet d'écouter un provider,
et à chaque fois que le provider change, exécute une fonction.
Les paramètres previous
et next
de cette fonction correspondent à la
dernière valeur avant que le provider ne change et à la nouvelle valeur après qu'il ait changé.