Construire un système de composants réactifs avec Kotlin



Bonjour à tous! Je m'appelle Anatoly Varivonchik, je suis développeur Android chez Badoo. Aujourd'hui, je vais partager avec vous la traduction de la deuxième partie de l'article de mon collègue Zsolt Kocsi sur la mise en œuvre de MVI, que nous utilisons quotidiennement dans le processus de développement. La première partie est ici .

Ce que nous voulons et comment nous le faisons


Dans la première partie de l'article, nous avons présenté les fonctionnalités , les éléments centraux de MVICore qui peuvent être réutilisés. Ils peuvent avoir la structure la plus simple et inclure un seul réducteur , ou ils peuvent devenir un outil entièrement fonctionnel pour gérer les tâches asynchrones, les événements et bien plus encore.

Chaque fonctionnalité est traçable - il est possible de s'abonner aux modifications de son statut et de recevoir des notifications à ce sujet. Dans ce cas, Feature peut être abonné à la source d'entrée. Et cela a du sens, car avec l'inclusion de Rx dans la base de code, nous avons déjà beaucoup d'objets et d'abonnements observables à différents niveaux.

C'est en lien avec l'augmentation du nombre de composants réactifs qu'il est temps de réfléchir à ce que nous avons et s'il est possible de rendre le système encore meilleur.

Nous devons répondre à trois questions:

  1. Quels éléments faut-il utiliser lors de l'ajout de nouveaux composants réactifs?
  2. Quelle est la façon la plus simple de gérer vos abonnements?
  3. Est-il possible d'ignorer la gestion du cycle de vie / la nécessité d'effacer les abonnements pour éviter les fuites de mémoire? En d'autres termes, pouvons-nous séparer la liaison des composants de la gestion des abonnements?

Dans cette partie de l'article, nous examinerons les bases et les avantages de la construction d'un système utilisant des composants réactifs et verrons comment Kotlin y contribue.

Éléments principaux


Au moment où nous avons commencé à travailler sur la conception et la standardisation de nos fonctionnalités , nous avions déjà essayé de nombreuses approches différentes et décidé que les fonctionnalités se présenteraient sous la forme de composants réactifs. Tout d'abord, nous nous sommes concentrés sur les principales interfaces. Tout d'abord, nous devions déterminer les types de données d'entrée et de sortie.

Nous avons raisonné comme suit:

  • Ne réinventons pas la roue - voyons quelles interfaces existent déjà.
  • Puisque nous utilisons déjà la bibliothèque RxJava, il est logique de se référer à ses interfaces de base.
  • Le nombre d'interfaces doit être minimisé.

En conséquence, nous avons décidé d'utiliser ObservableSource <T> pour la sortie et Consumer <T> pour l'entrée. Pourquoi pas Observable / Observer , demandez-vous. Observable est une classe abstraite dont vous devez hériter, et ObservableSource est l'interface que vous implémentez qui satisfait pleinement le besoin d'implémenter un protocole réactif.

package io.reactivex; import io.reactivex.annotations.*; /** * Represents a basic, non-backpressured {@link Observable} source base interface, * consumable via an {@link Observer}. * * @param <T> the element type * @since 2.0 */ public interface ObservableSource<T> { /** * Subscribes the given Observer to this ObservableSource instance. * @param observer the Observer, not null * @throws NullPointerException if {@code observer} is null */ void subscribe(@NonNull Observer<? super T> observer); } 

Observer , la première interface qui vient à l'esprit, implémente quatre méthodes: onSubscribe, onNext, onError et onComplete. Afin de simplifier le plus possible le protocole, nous avons préféré Consumer <T> , qui accepte de nouveaux éléments en utilisant une seule méthode. Si nous choisissions Observer , alors les méthodes restantes seraient le plus souvent redondantes ou fonctionneraient différemment (par exemple, nous aimerions présenter les erreurs dans le cadre de l'état, et non comme exceptions, et certainement pas interrompre le flux).

 /** * A functional interface (callback) that accepts a single value. * @param <T> the value type */ public interface Consumer<T> { /** * Consume the given value. * @param t the value * @throws Exception on error */ void accept(T t) throws Exception; } 

Nous avons donc deux interfaces, chacune contenant une méthode. Nous pouvons maintenant les lier en signant Consumer <T> à ObservableSource <T> . Ce dernier n'accepte que les instances d' Observer <T> , mais nous pouvons l'envelopper dans un Observable <T> , qui est abonné à Consumer <T> :

 val output: ObservableSource<String> = Observable.just("item1", "item2", "item3") val input: Consumer<String> = Consumer { System.out.println(it) } val disposable = Observable.wrap(output).subscribe(input) 

(Heureusement, la fonction .wrap (sortie) ne crée pas un nouvel objet si la sortie est déjà un Observable <T> ).

Vous vous souvenez peut-être que le composant Feature de la première partie de l'article utilisait des données d'entrée de type Wish (correspondant à Intent from Model-View-Intent) et des sorties de type State , et donc il peut être des deux côtés du bundle:

 // Wishes -> Feature val wishes: ObservableSource<Wish> = Observable.just(Wish.SomeWish) val feature: Consumer<Wish> = SomeFeature() val disposable = Observable.wrap(wishes).subscribe(feature) // Feature -> State consumer val feature: ObservableSource<State> = SomeFeature() val logger: Consumer<State> = Consumer { System.out.println(it) } val disposable = Observable.wrap(feature).subscribe(logger) 

Ce lien entre Consumer et Producer semble déjà assez simple, mais il existe un moyen encore plus simple de ne pas créer les abonnements manuellement ou les annuler.

Présentation de Binder .

Liaison stéroïde


MVICore contient une classe appelée Binder qui fournit une API simple pour gérer les abonnements Rx et possède un certain nombre de fonctionnalités intéressantes.

Pourquoi est-il nécessaire?

  • Créez une liaison en vous abonnant à l'entrée du week-end.
  • La possibilité de se désinscrire à la fin du cycle de vie (quand c'est un concept abstrait et n'a rien à voir avec Android).
  • Bonus: Binder vous permet d'ajouter des objets intermédiaires, par exemple, pour la journalisation ou le débogage dans le temps.

Au lieu de signer manuellement, vous pouvez réécrire les exemples ci-dessus comme suit:

 val binder = Binder() binder.bind(wishes to feature) binder.bind(feature to logger) 

Grâce à Kotlin, tout semble très simple.

Ces exemples fonctionnent si le type d'entrée et de sortie est le même. Et si ce n'est pas le cas? En implémentant la fonction d'extension, nous pouvons rendre la transformation automatique:

 val output: ObservableSource<A> = TODO() val input: Consumer<B> = TODO() val transformer: (A) -> B = TODO() binder.bind(output to input using transformer) 

Faites attention à la syntaxe: elle se lit presque comme une phrase normale (et c'est une autre raison pour laquelle j'aime Kotlin). Mais Binder n'est pas seulement utilisé comme sucre syntaxique - il nous est également utile pour résoudre les problèmes de cycle de vie.

Créer un classeur


La création d'une instance n'est nulle part plus simple:

 val binder = Binder() 

Mais dans ce cas, vous devez vous désabonner manuellement et vous devez appeler binder.dispose() chaque fois que vous devez supprimer des abonnements. Il existe une autre façon: injecter l'instance de cycle de vie dans le constructeur. Comme ça:

 val binder = Binder(lifecycle) 

Désormais, vous n'avez plus à vous soucier des abonnements - ils seront supprimés à la fin du cycle de vie. Dans le même temps, le cycle de vie peut être répété plusieurs fois (comme le cycle de démarrage et d'arrêt dans l'interface utilisateur Android) - et Binder créera et supprimera des abonnements pour vous à chaque fois.

Et qu'est-ce qu'un cycle de vie?


La plupart des développeurs Android, voyant l'expression "cycle de vie", représentent les cycles d'activité et de fragmentation. Oui, Binder peut travailler avec eux, en se désinscrivant à la fin du cycle.

Mais ce n'est qu'un début, car vous n'utilisez en aucun cas l'interface Android LifecycleOwner - Binder en a une qui est la plus universelle. Il s'agit essentiellement d'un flux de signaux BEGIN / END:

 interface Lifecycle : ObservableSource<Lifecycle.Event> { enum class Event { BEGIN, END } // Remainder omitted } 

Vous pouvez soit implémenter ce flux à l'aide d'Observable (par mappage), soit simplement utiliser la classe ManualLifecycle de la bibliothèque pour les environnements non Rx (voir exactement ci-dessous).

Comment fonctionne le liant ? En recevant un signal BEGIN, il crée des abonnements pour les composants que vous avez précédemment configurés ( entrée / sortie ), et en recevant un signal END, il les supprime. La chose la plus intéressante est que vous pouvez tout recommencer:

 val output: PublishSubject<String> = PublishSubject.create() val input: Consumer<String> = Consumer { System.out.println(it) } val lifecycle = ManualLifecycle() val binder = Binder(lifecycle) binder.bind(output to input) output.onNext("1") lifecycle.begin() output.onNext("2") output.onNext("3") lifecycle.end() output.onNext("4") lifecycle.begin() output.onNext("5") output.onNext("6") lifecycle.end() output.onNext("7") // will print: // 2 // 3 // 5 // 6 

Cette flexibilité dans la réaffectation des abonnements est particulièrement utile lorsque vous travaillez avec Android, lorsqu'il peut y avoir plusieurs cycles Start-Stop et Resume-Pause, en plus de l'habituel Create-Destroy.

Cycles de vie du classeur Android


Il y a trois classes dans la bibliothèque:

  • CreateDestroyBinderLifecycle ( androidLifecycle )
  • StartStopBinderLifecycle ( androidLifecycle )
  • ResumePauseBinderLifecycl e ( androidLifecycle )

androidLifecycle est la valeur renvoyée par la méthode getLifecycle() , c'est-à-dire AppCompatActivity , AppCompatDialogFragment , etc. Tout est très simple:

 fun createBinderForActivity(activity: AppCompatActivity) = Binder(   CreateDestroyBinderLifecycle(activity.lifecycle) ) 

Cycles de vie individuels


Ne nous arrêtons pas là, car nous ne sommes en aucun cas liés à Android. Quel est le cycle de vie d'un liant ? Littéralement n'importe quoi: par exemple, le temps de lecture d'une boîte de dialogue ou le temps d'exécution d'une tâche asynchrone. Vous pouvez, par exemple, le lier à l'étendue DI - puis tout abonnement sera supprimé avec lui. Liberté d'action totale.

  1. Vous souhaitez que les abonnements soient enregistrés avant que l' Observable n'envoie l'article? Convertissez cet objet en cycle de vie et passez-le à Binder . Implémentez le code suivant dans la fonction d' extension et utilisez-le plus tard:

     fun Observable<T>.toBinderLifecycle() = Lifecycle.wrap(this   .first()   .map { END }   .startWith(BEGIN) ) 
  2. Vous souhaitez conserver vos fixations jusqu'à la fin de Completable ? Aucun problème - cela se fait par analogie avec le paragraphe précédent:

     fun Completable.toBinderLifecycle() = Lifecycle.wrap(   Observable.concat(       Observable.just(BEGIN),       this.andThen(Observable.just(END))   ) ) 
  3. Vous voulez un autre code non Rx pour décider quand supprimer les abonnements? Utilisez ManualLifecycle comme décrit ci-dessus.

Dans tous les cas, vous pouvez soit appliquer un flux réactif au flux d'éléments Lifecycle.Event , soit utiliser ManualLifecycle si vous travaillez avec du code non Rx.

Présentation générale du système


Binder masque les détails de la création et de la gestion des abonnements Rx. Il ne reste qu'un bref aperçu général: «Le composant A interagit avec le composant B dans le champ d'application C».

Supposons que nous ayons les composants réactifs suivants pour l'écran actuel:



Nous aimerions que les composants soient connectés dans l'écran actuel, et nous savons que:

  • UIEvent peut être envoyé directement à AnalyticsTracker ;
  • UIEvent peut être transformé en Wish for Feature ;
  • L'état peut être transformé en un ViewModel pour une vue .

Cela peut s'exprimer en quelques lignes:

 with(binder) {   bind(feature to view using stateToViewModelTransformer)   bind(view to feature using uiEventToWishTransformer)   bind(view to analyticsTracker) } 

Nous faisons de telles pressions pour démontrer l'interconnexion des composants. Et comme nous, les développeurs, passons plus de temps à lire du code qu'à l'écrire, un bref aperçu est extrêmement utile, d'autant plus que le nombre de composants augmente.

Conclusion


Nous avons vu comment Binder aide à gérer les abonnements Rx et comment il vous aide à obtenir un aperçu d'un système construit à partir de composants réactifs.

Dans les articles suivants, nous expliquerons comment séparer les composants d'interface utilisateur réactifs de la logique métier et comment ajouter des objets intermédiaires à l'aide de Binder (pour la journalisation et le débogage de voyage dans le temps). Ne changez pas!

En attendant, consultez la bibliothèque sur GitHub .

Source: https://habr.com/ru/post/fr430550/


All Articles