Riverpod (state management)

Introduction

Flutter est aujourd’hui un Framework très populaire pour le développement d’applications mobiles performantes.

Au fil des développements, les applications proposent de plus en plus de fonctionnalité et gagnent donc en complexité. C’est alors que la gestion des états au sein de l’application devient cruciale, car elle permet d’assurer la cohérence et l’intégrité des données pour l’utilisateur.

Sans gestion d'état, il devient difficile pour le développeur de gérer des interactions complexes dans une application.

Aujourd’hui, de nombreuses librairies de state management sont proposées pour les développeurs d’applications Flutter. Chacune d’entre elles permettent de gérer l'état de manière efficace mais chacune a ses propres forces, faiblesses et cas d’utilisation.

Dans cet article, nous nous pencherons sur l’utilisation de Riverpod, l’une des librairies de state management les plus utilisées et appréciées des développeurs Flutter. L’objectif est ici de fournir une vision globale sur cette solution.

Nous l’illustrerons sur un POC d’application simple permettant d’afficher une liste de films, une fiche de détails de film, en utilisant l’API TMDB.

Riverpod

La librairie Riverpod a été créée par Rémi Rousselet et a vu sa première version apparaitre en août 2020.

Avec sa récente version majeure 2.0, elle est devenue plus complète, plus stable et plus simple à utiliser.

Anagramme de Provider, Riverpod s’est revendiqué comme une amélioration de son prédécesseur et suit le “provider pattern” qui revendique l’idée de centraliser à un endroit unique l'état de l’application et éviter ainsi qu’il soit dispersé dans l’application. Cela simplifie globalement le raisonnement et organise plus proprement le code du développeur.

Riverpod utilise des fournisseurs, appelés Provider. Ce sont eux qui permettent de gérer l'état de l’application. Ils peuvent être imbriqués les uns dans les autres pour représenter des relations complexes entre différents éléments de l'état. La librairie a la capacité de gérer de manière évolutive et efficace les dépendances entre les providers.

Intégration de la librairie

Riverpod est séparé en plusieurs librairies, en fonction de l’usage que l’on veut en faire. Pour l’utiliser dans notre projet Flutter, il a fallu ajouter le package flutter_riverpod dans le fichier pubspec.yaml.

Il est possible d’utiliser Riverpod avec des hooks, qui facilitent la déclaration et l’utilisation de certains provider, dans ce cas il faut ajouter les librairies flutter_hooks et hooks_riverpod. Pour commencer, nous avons choisi d’utiliser Riverpod sans les hooks afin de bien en prendre en main et comprendre l’outil.

Exposer un provider

Comme nous l'évoquions, les providers sont la partie centrale dans l’utilisation de Riverpod. Ils permettent d’encapsuler et d’exposer un état de l’application.

Les providers remplacent ainsi à eux seuls plusieurs patterns, tels que les singletons, l'injection de dépendances ou encore les « inheritedwidgets ».

Ils vont aussi permettre de simplifier la combinaison des états avec une syntaxe simple, et d’optimiser l’application en limitant la reconstruction des widgets.

Les providers doivent être déclarés avec cet objet en paramètre :

final cityProvider = Provider( ( ref) => 'London' ) ;

Pour faire fonctionner et rendre disponible les providers dans notre architecture de widgets dans l’applications, il est nécessaire d’ajouter le widget ProviderScope au niveau le plus haut de notre application :

 void main() {
  runApp(ProviderScope(child: MyApp()));
}

Il existe plusieurs types de providers, nous allons parler de ceux que l’on a utilisé. L’ intégralité des providers mis à disposition par Riverpod sont listés ici.

FutureProvider

Un FutureProvider est un type de provider proposé par Riverpod qui vous permet de gérer des valeurs asynchrones dans votre application Flutter. Contrairement à d'autres types de providers, tel que le Provider standard qui n'accepte que des valeurs synchrones, FutureProvider renvoie une instance de type Future.

Dans notre POC, nous l'avons utilisé afin de récupérer de manière asynchrones la liste des films.

StreamProvider

Un StreamProvider permet d'écouter un flux d'information qui a besoin d'être mis à jour en continu.

Dans notre POC, nous avons mis en place un décompte pour indiquer la sortie du film en utilisant un stream sur la date/heure de sortie de ce dernier. Puis nous l'avons combiné avec un ref du côté de notre widget pour récupérer le stream.

Accéder à un provider

Pour pouvoir accéder à un provider ou interagir avec, il est avant tout nécessaire d’obtenir l’objet “ref”.

Ce dernier va ensuite pouvoir être utilisé pour interagir avec le provider :

  • ref.watch pour reconstruire le widget qui s’est abonné au provider lorsque sa valeur change

final counter = ref.watch(counterProvider);
print('Counter value = $counter');

  • ref.listen pour exécuter une opération spécifique dès que la valeur du provider évolue (contrairement à ref.watch, ne reconstruit pas le widget)

ref.listen <int>(counterProvider, (int? previousValue, int currentValue) {
    print('Counter value = $currentValue');
});

  • ref.read pour obtenir la valeur du provider à un instant t en ignorant les changements à venir ou pour appeler une méthode du provider. A utiliser le moins possible selon la documentation pour des soucis de performances. Peut être nécessaire dans les cas suivants par exemple : lecture de la valeur du provider au clic sur un bouton, dans le initState d'un widget.

ref.read(counterProvider.notifier).increment();

Plusieurs façons sont proposées par Riverpod pour accéder à l'objet ref. En voici quelques-unes :

Depuis un widget

  • ·       StatelessWidget: en héritant du ConsumerWidget au lieu de StatelessWidget
class HomePage extends ConsumerWidget {
  const HomePage({
    super.key,  
  }); 
 
  @override  Widget build(BuildContext context, WidgetRef ref) {
    final providerValue = ref.watch(ourProvider);   // <-- use ref to access to a provider
    return Text ('Our provider value is : $providerValue');
  }
}

Le ConsumerWidget est similaire à un StatelessWidget. La seule différence est que sa méthode build possédera un paramètre supplémentaire : l'objet ref.

  • StatefulWidget: en héritant de ConsumerStatefulWidget eu lieu de StatefulWidget+ ConsumerState au lieu de State
class HomePage extends ConsumerStatefulWidget {
  const HomePage({
    super.key,
  });

  @override
  HomePageState createState() => HomePageState();
}

class HomePageState extends ConsumerState<HomePage> {
  @override
  void initState() {
    super.initState();
    ref.read(ourProvider);  // <-- use ref to access to a provider
  }

  @override
  Widget build(BuildContext context) {
    final providerValue = ref.watch(ourProvider);   // <-- use ref to access to a provider
    return Text('Our provider value is : $providerValue');
  }
}

De la même manière, l’objet ref est mis à disposition, cette fois en tant que propriété de la classe ConsumerState.

  • ·       En utilisant le widget Consumer
class HomePage extends StatelessWidget {
  const HomePage({
    super.key,  
  }); 
  
  @override  Widget build(BuildContext context) {
    return Consumer(
      builder: (context, ref, child) {
        final providerValue = ref.watch(ourProvider);   // <-- use ref to access to a provider
        return Text('Our provider value is : $providerValue');
      }
    );
  }
}

Depuis un autre provider

final ourProvider = Provider((ref) {
  final providerValue = ref.watch(anotherProvider);   // <-- use ref to access to another provider
  return providerValue + 4500;
});

Retour d’expérience

Habitués à utiliser la librairie “concurrente” flutter_bloc, permettant également de faire du state management, nous avons pu au cours de cet après-midi de pratique, comparer la mise en œuvre et les principes de Riverpod vs. Flutter BLoC.

Modularité et testabilité

Essayant de respecter au mieux la Clean Architecture, nous nous efforçons de séparer les responsabilités des couches logicielles. Que nous utilisions BLoC ou Riverpod, la gestion des états de l’application est bien séparée de la présentation, l’UI. Cette séparation permet d’avoir une base de code plus évolutive, maintenable et testable.

Favorisant tous deux une architecture modulaire, BLoC et Riverpod promeuvent la facilité d'écriture de tests automatisés, permettant de tester la logique de l’application de manière isolée, sans avoir à se préoccuper de l’UI. L’objectif étant toujours de réduire le risque de bogues et d’améliorer la qualité globale du code.

Avec la librairie BLoC, le state management est assuré par une architecture pilotée par les événements, où le widget s'abonne à ces changements de mises à jour de l'état et se reconstruit ainsi chaque fois que l'état change.

Avec Riverpod, le state management est réalisé par Provider, une solution de state management directement intégrée à Flutter. Les providers gèrent l'état et l'exposent à leurs enfants dans l'arbre des widgets via le contexte.

Injection de dépendances

BLoC et Riverpod permettent tous deux de transmettre de l'état de l’application à l’arbre de widgets comme s’il s’agissait d’une injection de dépendances.

Avec BLoC, les blocs peuvent être exposés et sont rendus accessibles par l’intermédiaire de BlocProvider.

Avec Riverpod, l'état est exposé de la même manière en utilisant les providers.

Simplicité

Une utilisation complète de la librairie BLoC demandera au développeur d'écrire les événements, les états qui en découlent et le BLoC pour l’implémentation de la logique intermédiaire.

Riverpod quant à lui ne gère pas d'événements mais uniquement des états. Pour l'équipe de développeurs, cela peut rendre le code plus simple, moins verbeux mais aussi moins structuré qu’en utilisant BLoC selon l’utilisation qu’on en fait. Là encore, c’est une histoire de goûts !

Riverpod propose directement dans sa solution une base de 3 états (fail, loaded et loading) via la classe AsyncValue, ce qui peut s’avérer pratique à l’utilisation.

Dans sa dernière version, Riverpod propose de la génération de code pour simplifier encore plus le travail du développeur en exploitant les annotations proposées.

Synthèse

Finalement, Riverpod et BLoC sont deux solutions de state management populaires pour des applications Flutter, ayant chacune d’entre elles des forces et des faiblesses. Il n’y a pas une solution meilleure que l’autre. Choisir l’une ou l’autre des solutions doit se faire selon les besoins, les contraintes spécifiques du projet et les appétences des développeurs.

Les deux librairies ont prouvé leur efficacité et ont à présent une communauté d’utilisateurs et de développeurs relativement riche et grandissante, avec des mises à jour régulières permettant de répondre aux évolutions de l'écosystème Flutter.

L'échelle et la complexité du projet doivent bien entendu être un critère de choix. Quelque soit la solution de gestion des états retenue, il est primordial de comprendre les concepts sous-jacents de state management pour assurer un développement d’applications performantes, évolutives et maintenables, avec une bonne expérience utilisateur.

 

 

NOTE

Dans le cadre du POC, nous avons utilisé les librairies suivantes :

  • dio et retrofit pour les requêtes à l’API TMDB
  • go_router pour la navigation in-app
  • freezed pour la génération des modèles de données

 

 

Luc ALLARDIN – Développeur Mobile

Anthony AUMOND – Développeur Mobile

Fabien DHERMY – Développeur Mobile

Majid SERHANE – Développeur Mobile

Sources : https://riverpod.dev/fr/docs/getting_started