
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:
- Quels éléments faut-il utiliser lors de l'ajout de nouveaux composants réactifs?
- Quelle est la façon la plus simple de gérer vos abonnements?
- 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.*; public interface ObservableSource<T> { 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).
public interface Consumer<T> { 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:
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 }
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")
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.
- 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) )
- 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)) ) )
- 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 .