Reactive Programming - Streams - BLoC

Compatibilité
Date révision
27 mars 2023
Publié le
20 août 2018
Flutter
v 3.13.x
Dart
v 3.1.x

Introduction

Il m'a fallu un certain temps pour trouver un façon d'introduire les notions de Reactive Programming, BLoC et Streams.

Comme il s’agit d’un changement radical dans la façon d'architecturer une application, je voulais un exemple pratique qui montre que:

  • il est très possible de ne pas utiliser ces notions mais ce peut-être parfois plus difficile à coder et moins performant,
  • les avantages de les utiliser, mais aussi
  • les impacts de leur utilisation (positive et / ou négative).

L'exemple pratique que j'ai réalisé est une pseudo-application qui, en résumé, permet à un utilisateur d'afficher une liste de films à partir d'un catalogue en ligne, de les filtrer par genre et par date de sortie, de les marquer comme favoris. Bien sûr, tout est interactif, les actions des utilisateurs peuvent se dérouler dans différentes pages ou à l'intérieur d'une même page et avoir un impact sur les aspects visuels, en temps réel.

Ceci est une animation qui montre cette application.


Streams Application

Puisque vous êtes arrivés sur cette page pour obtenir des informations sur Reactive Programming, BLoC et Streams, je commencerai par une présentation. Par la suite, je vous montrerai comment les mettre en œuvre et les utiliser, dans la pratique.


Qu'est-ce qu'un Stream ?

Introduction

Afin de visualiser facilement la notion de Stream, considérez simplement un tuyau à 2 extrémités, une seule permettant d'y insérer quelque chose. Lorsque vous insérez quelque chose dans le tuyau, il transite à l'intérieur du tuyau et sort par l'autre extrémité.

En Flutter,

  • le tube s'appelle Stream
  • pour contrôler le Stream, on utilise habituellement (*) un StreamController
  • pour insérer quelque chose dans le Stream, le StreamController expose une "entrée", appelée StreamSink, accessible via la propriété sink
  • la sortie du Stream, est exposée par le StreamController via la propriété stream

(*): J'ai intentionnellement utilisé le terme "habituellement", car il est très possible de ne pas utiliser StreamController. Cependant, comme vous le lirez dans cet article, je n’utiliserai que des StreamControllers.

Que peut-on transmettre par un Stream ?

Tout et n'importe quoi. Que ce soit une valeur, un événement (=event), un objet, une collection, une Map, une erreur ou même un autre Stream, n'importe quel type de données peut être transmis par un Stream.

Comment puis-je savoir si quelque chose est véhiculé par un Stream ?

Lorsque vous devez être averti que quelque chose est transmis par un Stream, vous devez simplement écouter(= listen) la propriété stream du StreamController.

Lorsque vous définissez un listener, vous recevez un objet de type StreamSubscription. C'est via cet objet StreamSubscription que vous serez averti que quelque chose se passe au niveau du Stream.

Dès qu'il y a au moins un listener actif, le Stream commence à générer des événements pour notifier l'objet StreamSubscription actif à chaque fois que:

  • certaines données sortent du stream,
  • lorsqu'une erreur a été envoyée au stream,
  • lorsque le stream est fermé.

L'objet StreamSubscription vous permet également de:

  • arrêter d'écouter,
  • de faire une pause,
  • de redémarrer l'écoute.

Est-ce qu'un Stream n'est qu'un simple tuyau?

Non, un Stream permet également de traiter les données qui y circulent avant leur sortie.

Pour contrôler le traitement des données à l'intérieur d'un Stream, on utilise un StreamTransformer, qui n'est rien d'autre que

  • une fonction qui "capture" les données qui circulent dans le Stream
  • fait quelque chose avec les données
  • le résultat de cette transformation est également un Stream

Vous comprendrez directement à partir de cette déclaration qu'il est très possible d'utiliser plusieurs StreamTransformers en séquence.

Un StreamTransformer peut être utilisé pour effectuer tout type de traitement, par exemple:

  • filtrage: pour filtrer les données en fonction de tout type de condition,
  • regroupement: regrouper des données,
  • modification: pour appliquer tout type de modification aux données,
  • injecter des données dans d'autres streams,
  • mise en mémoire tampon,
  • traitement: faire toute sorte d'action / opération basée sur les données,
  • ...

Types de Streams

Il existe 2 types de Streams.

Streams à abonnement unique (=Single Subscription)

Ce type de Stream permet uniquement un listener pendant toute la durée de vie de ce Stream.

Il n'est pas possible d'écouter deux fois sur un tel Stream, même après l'annulation du premier StreamSubscription.

Streams de diffusion (=Broadcast)

Ce deuxième type de Stream permet n'importe quel nombre de listeners.

Il est possible d'ajouter un listener à un Broadcast Stream à tout moment. Le nouveau listener recevra les événements dès qu’il commencera à écouter le Stream.

Exemples de base

N'importe quel type de données

Ce tout premier exemple montre un Stream de type "Single-subscription", qui imprime simplement les données qui sont entrées. Comme vous pouvez le voir, le type de données n'a pas d'importance.



StreamTransformer

Ce deuxième exemple montre un Stream de type "Broadcast" qui transmet des valeurs entières (=int) et imprime uniquement les nombres pairs.

Pour ce faire, nous appliquons un StreamTransformer qui filtre (ligne 14) les valeurs et ne laisse passer que les nombres pairs.



RxDart

De nos jours, l'introduction du Streams ne serait pas complète si je ne mentionnais pas le package RxDart.

Le package RxDart est une implémentation en Dart de l'API ReactiveX, qui étend les API Dart relatives au Streams originales afin de se conformer aux normes ReactiveX.

Comme cela n'a pas été défini à l'origine par Google, il utilise un vocabulaire différent. Le tableau suivant vous donne la corrélation entre Dart et RxDart.

DartRxDart
StreamObservable
StreamControllerSubject

RxDart comme dit précédemment, étend l'API originale Dart Streams et offre 3 variations principales de StreamController:

PublishSubject

Le PublishSubject est un broadcast StreamController normal à une exception près: stream retourne un Subject au lieu d'un Stream.


PublishSubject (c) ReactiveX.io

Comme vous pouvez le voir, PublishSubject envoie à un listener uniquement les événements qui ont été ajoutés au Stream après que le listener ait commencé à écouter.

BehaviorSubject

Le BehaviorSubject est un broadcast StreamController normal à une exception près: stream retourne un Subject au lieu d'un Stream.


BehaviorSubject (c) ReactiveX.io

La différence principale avec un PublishSubject est que le BehaviorSubject envoie également au listener le tout dernier événement qui ait été émis avant que le listener ait commencé à écouter..

ReplaySubject

Le ReplaySubject est un broadcast StreamController normal à une exception près: stream retourne un Subject au lieu d'un Stream.


ReplaySubject (c) ReactiveX.io

Le ReplaySubject, par défaut, envoie tous les événements qui ont déjà été émis par le Stream à tout nouveau listener quel que soit le moment où ils commencent à écouter.


Remarque importante sur la notion de Ressources

C'est toujours une bonne pratique de systématiquement libérer les ressources qui ne sont plus nécessaires.

Cette déclaration s'applique à:

  • StreamSubscription - lorsque vous n'avez plus besoin d'écouter un stream, annulez (=cancel) le StreamSubscription;
  • StreamController - lorsque vous n'avez plus besoin d'un StreamController, fermez-le (=close);
  • Il en va de même pour les Subject RxDart, lorsque vous n'avez plus besoin d'un BehaviourSubject, d'un PublishSubject ... fermez-le (=close)

Comment créer un widget basé sur les données qui sortent d'un Stream ?

Flutter offre un StatefulWidget très pratique, appelé StreamBuilder.

Un StreamBuilder écoute un Stream et, chaque fois que certaines données sortent de ce Stream, il se reconstruit automatiquement, appelant son callback builder.

Voici comment utiliser le StreamBuilder:



StreamBuilder<T>(
  key: ...optional, the unique ID of this Widget...
  stream: ...the stream to listen to...
  initialData: ...any initial data, in case the stream would initially be empty...
  builder: (BuildContext context, AsyncSnapshot<T> snapshot){
      if (snapshot.hasData){
          return ...the Widget to be built based on snapshot.data
      }
      return ...the Widget to be built if no data is available
  },
)

L'exemple suivant imite l'application par défaut "compteur", mais utilise un Stream et non plus un setState.



Explication et commentaires:

  • Lignes 24-30: nous écoutons le stream et chaque fois qu’une nouvelle valeur sort de ce stream, nous mettons à jour le Text avec cette valeur;
  • Ligne 35: lorsque nous cliquons sur le FloatingActionButton, nous incrémentons le compteur et l'envoyons au Stream, via le sink; le fait d'injecter une valeur dans le stream fait que le StreamBuilder qui l'écoute se reconstruit et "actualise" le compteur;
  • Nous n'avons plus besoin de la notion de State, tout est pris en charge via le Stream;
  • Ceci est une grande amélioration car le fait d'appeler la méthode setState() force le Widget entier (et tout sous-widget) à se reconstruire. Ici, SEULEMENT le StreamBuilder est reconstruit (et bien sûr ses widgets enfants);
  • La seule raison pour laquelle nous utilisons toujours un StatefulWidget pour la page est simplement parce que nous devons libérer le StreamController, via la méthode dispose, ligne 15;

Qu'est-ce que la programmation réactive (= Reactive Programming ) ?

La programmation réactive est la programmation avec des flux de données asynchrones.

En d'autres termes, tout, depuis un événement (par exemple, un clic), des modifications sur une variable, des messages, ... pour créer des demandes, tout ce qui peut changer ou se produire sera transmis, déclenché par un flux de données (=Stream).

En clair, tout cela signifie qu’avec la programmation réactive, l’application:

  • devient asynchrone,
  • est architecturée autour de la notion de Streams et listeners,
  • quand quelque chose arrive quelque part (un événement, un changement de variable ...) une notification est envoyée à un Stream,
  • si "quelqu'un" écoute ce Stream, il sera notifié et prendra les mesures appropriées, quel que soit son emplacement dans l'application.

Il n'y a plus de couplage étroit entre les composants.

En bref, lorsqu'un Widget envoie quelque chose à un Stream, ce Widget n'a plus besoin de savoir:

  • ce qui se passera par la suite,
  • qui pourrait utiliser ces informations (personne, un ou plusieurs Widgets ...)
  • où cette information pourrait être utilisée (nulle part, même écran, un autre, plusieurs…),
  • lorsque cette information pourrait être utilisée (presque directement, après plusieurs secondes, jamais ...).

... ce Widget ne se soucie que de ses propres affaires, c"est tout !!

À première vue, en lisant ceci, cela peut sembler conduire à un "non-contrôle" de l'application mais, comme nous le verrons, c'est le contraire. Cela permet:

  • la possibilité de construire des parties de l'application uniquement responsables d'activités spécifiques,
  • facilement simuler le comportement de certains composants pour permettre une couverture plus complète des tests,
  • réutiliser facilement des composants (ailleurs dans l'application ou dans une autre application),
  • pour modifier l'application et pouvoir déplacer les composants d'un endroit à un autre sans trop de refactoring,
  • ...

Nous verrons les avantages sous peu ... mais avant, abordons le dernier sujet: le BLoC Pattern.


Le BLoC Pattern

Le BLoC Pattern a été conçu par Paolo Soares et Cong Hui, de Google et présenté pour la première fois lors du DartConf 2018 (23-24 janvier 2018). Voir la vidéo sur YouTube.

BLoC signifie Business Logic Component.

En bref, la Business Logic (=logique métier) doit:

  • être déplacée vers un ou plusieurs BLoC s,
  • être supprimée autant que possible de la couche présentation. En d'autres termes, les composants de l'interface utilisateur ne doivent se soucier que des problèmes liés à l'interface utilisateur et non de la logique applicative/métier.
  • s'appuyer sur l'utilisation exclusive de Streams pour les entrées (Sink) et les sorties (stream),
  • rester indépendante de la plate-forme,
  • reste indépendante de l'environnement.

En effet, le pattern BLoC a été initialement conçu pour permettre de réutiliser le même code indépendamment de la plateforme: application web, application mobile, back-end.

Qu'est-ce que cela signifie vraiment ?

Le "BLoC pattern" utilise les notions que nous venons d'aborder précédemment: Streams.



  • Les Widgets envoient des events (=événements) au BLoC via Sinks,
  • Les Widgets sont notifiés par le BLoC via streams,
  • La "business logic" implémentée par le BLoC n'est pas de leur ressort.

À partir de cette déclaration, nous pouvons directement voir un énorme avantage.

Grâce au découplage de la Business Logic de l’UI:

  • nous pouvons changer la logique métier à tout moment, avec un impact minimal sur l'application,
  • nous pouvons modifier l'interface utilisateur sans impact sur la logique métier,
  • il est maintenant beaucoup plus facile de tester la logique métier.

Comment appliquer ce modèle BLoC à l'exemple d'application Counter?

Appliquer le motif BLoC à cette application Counter peut sembler exagéré, mais laissez-moi d'abord vous montrer ...



Je vous entends déjà dire "wouaw ... pourquoi tout ça? Est-ce que tout cela est nécessaire ?".

Premièrement, la séparation des responsabilités

Si vous jetez un coup d'oeil au code de CounterPage (lignes 21 à 45), vous vous apercevrez qu'il n'y a absolument aucune logique métier.

Cette page est maintenant uniquement responsable de:

  • afficher le compteur, qui n'est maintenant rafraîchi que si nécessaire (même sans que la page ait à le savoir)
  • fournir un bouton qui, une fois touché, demande une action à effectuer sur le compteur

En outre, la Business Logic est centralisée en une seule classe "IncrementBloc".

Si maintenant, vous devez changer la logique métier, il vous suffit de mettre à jour la méthode _handleLogic (lignes 72-75). Peut-être que la nouvelle logique métier demandera de faire des choses très complexes ... Le CounterPage ne le saura jamais et c'est très bien!

Deuxièmement, la testabilité

Il est maintenant beaucoup plus facile de tester la logique métier.

Plus besoin de tester la logique métier via l'interface utilisateur. Seule la classe IncrementBloc doit être testée.

Troisièmement, la liberté d'organiser la mise en page

Grâce à l'utilisation de Streams, vous pouvez maintenant organiser la mise en page indépendamment de la logique métier.

Toute action peut être lancée depuis n'importe quel endroit de l'application: appelez simplement le sink .incrementCounter.

Vous pouvez afficher le compteur n'importe où, dans n'importe quelle page, écoutez simplement le stream .outCounter.

Quatrièmement, la réduction du nombre de "builds"

Le fait de ne pas utiliser setState() mais StreamBuilder réduit considérablement la quantité de "build" et limite à ceux réellement nécessaires.

Du point de vue de la performance, il s'agit d'une amélioration considérable.


Il n'y a qu'une contrainte ... l'accessibilité du BLoC

Pour que tout cela fonctionne, le BLoC doit être accessible.

Il existe plusieurs façons de le rendre accessible:

  • via un singleton global

    Cette façon est très possible mais pas vraiment recommandée. Comme il n'y a pas de destructeur de classe (=class destructor) dans Dart, vous ne pourrez jamais libérer les ressources correctement.

  • en tant qu'instance locale

    Vous pouvez instancier une instance locale d'un BLoC. Dans certaines circonstances, cette solution répond parfaitement à certains besoins. Dans ce cas, vous devriez toujours envisager d'initialiser dans un StatefulWidget afin que vous puissiez profiter de la méthode dispose() pour la libérer.

  • fourni par un ancêtre (=ancestor)

    La manière la plus courante de le rendre accessible est d'utiliser un widget ancêtre, implémenté sous la forme d'un StatefulWidget.

    Le code suivant montre un exemple d'un BlocProvider générique.



Quelques explications sur ce BlocProvider générique

Tout d'abord, comment l'utiliser en tant que provider (=fournisseur) ?

Si vous regardez l'exemple de code "streams_4.dart", vous verrez les lignes de codes suivantes (lignes # 12-15)



home: BlocProvider<IncrementBloc>(
  bloc: IncrementBloc(),
  child: CounterPage(),
),

Ces lignes créent simplement une nouvelle instance de BlocProvider qui gérera IncrementBloc, et affichera le CounterPage en tant qu'enfant (=child).

A partir de ce moment, tout widget faisant partie du sous-arbre, à partir de BlocProvider pourra accéder au IncrementBloc, via la ligne suivante:



IncrementBloc bloc = BlocProvider.of<IncrementBloc>(context)!;

Peut-on avoir plusieurs BLoC?

Bien sûr et c'est vivement conseillé. Les recommandations sont les suivantes:

  • (s'il y a une logique métier) un BLoC en haut de chaque page,
  • pourquoi pas un ApplicationBloc pour gérer l'état de l'application?
  • chaque composant "assez complexe" a un BLoC correspondant.

L'exemple de code suivant montre un ApplicationBloc en tête de toute l'application, puis le IncrementBloc en tête du CounterPage.

L'exemple montre également comment récupérer les deux blocs.



Pourquoi ne pas utiliser un InheritedWidget?

Dans la plupart des articles liés à BLoC, vous verrez l'implémentation du Provider en tant que InheritedWidget.

Bien sûr, rien n'empêche ce type d'implémentation. cependant,

  • un InheritedWidget ne fournit pas de méthode dispose et, rappelez-vous, il est recommandé de toujours libérer les ressources dès qu'elles ne sont plus nécessaires.
  • bien sûr, rien ne vous empêcherait de placer InheritedWidget dans un autre StatefulWidget, mais alors, quelle est la valeur ajoutée de l'utilisation d'un InheritedWidget?
  • enfin, l'utilisation d'un InheritedWidget entraîne souvent des effets secondaires si elle n'est pas contrôlée (voir Rappel sur InheritedWidget, ci-dessous).

Ces 3 points expliquent le choix que j'ai fait pour implémenter le BlocProvider générique en tant que StatefulWidget, afin que je puisse libérer les ressources, lorsque ce widget est disposé.

Flutter ne peut pas instancier un type générique <T>

Comme Flutter ne peut malheureusement pas instancier un type générique, nous devons passer l'instance du BLoC au BlocProvider. Pour imposer l'implémentation de la méthode dispose() dans chaque BLoC, tous les BLoCs doivent implémenter l'interface BlocBase.

Rappel sur InheritedWidget

Lorsque nous utilisons un InheritedWidget et invoquons la méthode context.inheritFromWidgetOfExactType(...) pour obtenir le widget le plus proche du type donné, cet appel de méthode enregistre automatiquement ce "context" (= BuildContext) à la liste de ceux qui seront reconstruits chaque fois qu'une modification s'applique à la sous-classe InheritedWidget ou à l'un de ses ancêtres.

Veuillez noter que, pour être tout à fait correct, le problème lié au InheritedWidget, comme je viens de l'expliquer, ne se produit que lorsque nous combinons le InheritedWidget avec un StatefulWidget. Lorsque vous utilisez simplement un InheritedWidget sans State, le problème ne se produit pas. Mais ... je reviendrai sur cette remarque dans un prochain article.

Le type de widget (Stateful ou Stateless) lié à BuildContext n'a pas d'importance.


Note personnelle sur BLoC

La troisième règle liée au BLoC Pattern indique: "repose sur l'utilisation exclusive des flux à la fois pour l'entrée (sink) et la sortie (stream)".

Mon expérience personnelle relativise un peu cette affirmation ... Laissez-moi vous expliquer.

Au début, le modèle BLoC a été conçu pour partager le même code entre les plates-formes (AngularDart, ...) et, dans cette perspective, cette déclaration prend tout son sens.

Cependant, si vous avez seulement l'intention de développer une application Flutter, c'est, selon mon humble expérience, un peu exagéré.

Si nous nous en tenons à l'instruction, aucun getter ou setter n'est possible, mais seulement sink et stream. L'inconvénient est "tout cela est asynchrone".

Prenons 2 exemples pour illustrer les inconvénients:

  • vous devez récupérer certaines données du BLoC pour utiliser ces données en entrée d'une page qui devrait afficher ces paramètres immédiatement (par exemple, pensez à une page parametres), si nous devions compter sur des Streams , cela rend le build de la page asynchrone (ce qui est complexe). Un exemple de code pour le faire fonctionner via Streams pourrait être le suivant ... Pas très beau, non ?


  • au niveau du BLoC, vous devez également convertir une "fausse" injection de certaines données pour déclencher la fourniture des données que vous prévoyez de recevoir via un stream. Un exemple de code pour réaliser ce cas:


Je ne sais pas ce que vous en pensez, mais personnellement, si je n'ai pas de contraintes liées au portage / partage de code, je trouve cela trop lourd et je préférerais utiliser les habituels getters / setters en cas de besoin et utiliser les Streams / Sinks pour maintenir la séparation des responsabilités et diffuser les informations si nécessaire, ce qui est génial.


Il est maintenant temps de voir tout cela en pratique...

Comme mentionné au début de cet article, j'ai construit une pseudo-application pour montrer comment utiliser toutes ces notions.

Le code source complet se trouve sur Github.

Soyez indulgents, car ce code est loin d'être parfait et pourrait être meilleur et/ou mieux conçu, mais le seul objectif est simplement de vous montrer comment tout cela fonctionne.

Comme le code source est beaucoup documenté, je n’expliquerai que les principes principaux.

Source du catalogue de films

J'utilise la API TMDB, qui est gratuite, pour récupérer la liste de tous les films, ainsi que les affiches, les classements et les descriptions.

Afin de pouvoir exécuter cet exemple d'application, vous devrez vous enregistrer et obtenir la clé API (qui est totalement gratuite), puis mettre votre clé API dans le fichier "/api/tmdb_api.dart", en ligne 15.


Architecture de l'application

Cette application utilise:

  • 3 BLoC principaux:

    • ApplicationBloc (en tête de tout), chargé de fournir la liste de tous les genres de films;
    • FavoriteBloc (juste en dessous), chargé de gérer la notion de "Favoris";
    • MovieCatalogBloc (en tête des 2 pages principales), chargé de fournir la liste des films, basée sur des filtres;
  • 6 pages:

    • HomePage: page initiale permettant la navigation vers les 3 sous-pages;
    • ListPage: page qui répertorie les films en tant que GridView, permet le filtrage, la sélection des favoris, l'accès aux favoris et l'affichage des détails du film dans une page secondaire;
    • ListOnePage: similaire à ListPage mais la liste des films est affichée sous forme de liste horizontale et les détails en-dessous;
    • FavoritesPage: page qui répertorie les favoris et permet la désélection de tous les favoris;
    • Filters: EndDrawer qui permet de définir des filtres: genres et dates de publication min / max. Cette page est appelée depuis ListPage ou ListOnePage;
    • Details: page uniquement invoquée par ListPage pour afficher les détails d'un film mais aussi pour permettre la sélection / désélection du film en tant que favori;
  • 1 sous BLoC:

    • FavoriteMovieBloc, lié à un MovieCardWidget ou MovieDetailsWidget pour gérer la sélection / désélection d'un film en tant que favori
  • 5 widgets principaux:

    • FavoriteButton: widget chargé d'afficher le nombre de favoris, en temps réel, et de rediriger vers la FavoritesPage lorsque vous appuyez dessus;
    • FavoriteWidget: widget chargé d'afficher les détails d'un film favori et de le désélectionner;
    • FiltersSummary: widget responsable de l'affichage des filtres actuellement définis;
    • MovieCardWidget: widget chargé d’afficher un seul film en tant que carte, avec l’affiche du film, l’évaluation et le nom, ainsi qu’une icône indiquant la sélection de ce film en tant que favori;
    • MovieDetailsWidget: widget chargé d’afficher les détails relatifs à un film particulier et de permettre sa sélection / désélection en tant que favori.

Orchestration des différents BLoC / Streams

Le diagramme suivant montre comment les 3 principaux BLoC sont utilisés:

  • sur le côté gauche d'un BLoC, quels composants invoquent le Sink
  • à droite, quels composants écoutent le stream

Par exemple, lorsque MovieDetailsWidget appelle le Sink inAddFavorite, 2 streams sont déclenchés:

  • outTotalFavorites qui force une reconstruction de FavoriteButton, et
  • outFavorites qui
    • force la reconstruction de MovieDetailsWidget (l'icône "favorite")
    • force la reconstruction de _buildMovieCard (l'icône "favorite")
    • est utilisé pour construire chaque MovieDetailsWidget


Observations

La plupart des widgets et pages sont StatelessWidget, ce qui signifie que:

  • le setState() qui force à reconstruire n'est presque jamais utilisé. Les exceptions sont les suivantes:

    • dans le ListOnePage lorsqu'un utilisateur clique sur une MovieCard, pour actualiser MovieDetailsWidget. Cela pourrait aussi avoir été réalisé à l'aide d'un stream ...
    • dans le FiltersPage pour permettre à l'utilisateur de changer les filtres avant de les accepter, via Sink.
  • l'application n'utilise aucun InheritedWidget

  • l'application est presque 100% pilotée par BLoCs / Streams, ce qui signifie que la plupart des widgets sont indépendants les uns des autres et de leur emplacement dans l'application

    Un exemple pratique est le FavoriteButton qui affiche le nombre de favoris sélectionnés dans un badge. L'application compte 3 instances de ce FavoriteButton, chacune d'elles, affichée dans 3 pages différentes.

Affichage de la liste des films (explication de l'astuce pour afficher une liste infinie)

Pour afficher la liste des films répondant aux critères de filtrage, nous utilisons soit un GridView.builder (ListPage) ou un ListView.builder (ListOnePage) comme liste de défilement infinie.

Les films sont récupérés à l'aide de l'API TMDB par l'intermédiaire de pages de 20 films à la fois.

Pour rappel, GridView.builder et ListView.builder prennent en entrée un itemCount qui, s'il est fourni, indique le nombre d'éléments à afficher. Le itemBuilder est appelé avec index variant de 0 à itemCount - 1.

Comme vous le verrez dans le code, je fournis arbitrairement au GridView.builder un itemCount comptant 30 items supplémentaires. La raison en est que dans cet exemple, nous manipulons un nombre présumé infini d'éléments (ce qui n'est pas totalement vrai, mais ce n'est qu'un exemple). Cela forcera le GridView.builder à demander "jusqu'à 30 éléments supplémentaires" à afficher.

En outre, GridView.builder et ListView.builder appellent uniquement le itemBuilder lorsqu'ils considèrent qu'un élément (index) doit être rendu dans la fenêtre d'affichage.

Le stream MovieCatalogBloc.outMoviesList renvoie un List<MovieCard>, qui est itéré afin de générer chaque carte correspondant à un film. La toute première fois, cette liste est vide, mais grâce au itemCount: ... + 30, nous trompons le système qui demandera le rendu de 30 éléments non-existants via la méthode _buildMovieCard(...).

Comme vous le verrez dans le code, cette routine lance un appel étrange au Sink:



// Notify the MovieCatalogBloc that we are rendering the MovieCard[index]
    movieBloc.inMovieIndex.add(index);

Cet appel indique au MovieCatalogBloc que nous voulons afficher la MovieCard[index].

Ensuite, la méthode _buildMovieCard(...) continue à valider que les données liées à la MovieCard[index] existent. Si oui, le dernier est rendu, sinon un CircularProgressIndicator est affiché.

L'appel à MovieCatalogBloc.inMovieIndex.add(index) est écouté par un StreamSubscription qui traduit l'index en un certain nombre "pageIndex" (une page compte jusqu'à 20 films). Si la page correspondante n'a pas encore été récupérée via l'API TMDB, un appel à l'API est effectué. Une fois la page récupérée, la nouvelle liste de tous les films récupérés est envoyée à _moviesController. Comme stream (= movieBloc.outMoviesList) est écouté par le GridView.builder, ce dernier demande à reconstruire la MovieCard correspondante. Comme nous avons maintenant les données, nous pouvons les afficher,


Crédits et liens supplémentaires

Les images qui décrivent les PublishSubject, BehaviorSubject et ReplaySubject proviennent du site de ReactiveX.

Quelques autres articles intéressants à lire:


Conclusion

Très long article mais il y a encore tant et tant à dire à ce sujet.

Selon moi, il est plus qu'évident que c'est la voie à suivre pour développer une application Flutter. Cela offre tant de flexibilité.

Restez à l'écoute des nouveaux articles et bon codage.

0 Commentaires
Soyez le premier à faire un commentaire...
© 2024 - Flutteris
email: info@flutteris.com

Flutteris



Quand l'excellence rencontre l'innovation
"votre satisfaction est notre priorité"

© 2024 - Flutteris
email: info@flutteris.com