Salut, mon nom est Alexei Valyakin, j'écris des applications pour Android. Il y a quelques mois, j'ai parlé lors d'une réunion de l'équipe Yandex.Taxi avec des développeurs mobiles. Mon rapport était consacré à la transition vers l'architecture des RIB dans les taxis (RIB signifie Router, Interactor, Builder). Voici la vidéo, et sous la coupe - un recueil:
- Il est temps de sauter un peu dans un train avec un battage médiatique. Il s'agit d'un thème classique sur l'architecture dans Android.
Sérieusement, aujourd'hui, je veux vous expliquer comment et pourquoi nous avons mis en œuvre cette architecture, quelles difficultés nous avons rencontrées et comment elle peut vous aider.

Lorsque j'ai rejoint l'entreprise, notre équipe était composée de quatre personnes. Déjà à ce moment, nous avons eu un grand nombre de difficultés. Le projet était ancien, il a commencé en 2012. Il y a eu pas mal de problèmes techniques, dont un CI mal construit, beaucoup de variabilité dans les approches, des tests qui ne couvrent pas tout. Et en général, il y avait un grand nombre de difficultés et de conflits de fusion.
En deux ans, nous sommes passés à 12 personnes, ce qui signifie que nous avons augmenté la parallélisation du développement des fonctionnalités. Par conséquent, il y a encore plus de conflits de fusion, et avec beaucoup de cohérence du code, vous comprenez à quoi cela peut conduire. À un moment donné, nous avons juste commencé à couler et, d'une manière ou d'une autre, nous avons dû le comprendre. Une partie de ces problèmes a été résolue par une refactorisation ponctuelle, une partie par la bibliothèque de composants, qui mérite d'être discutée dans un rapport séparé.

Que veulent tous les développeurs? Belle architecture, flexibilité de développement, facilité d'ajout de fonctionnalités et, bien sûr, réduction de la complexité des fusions - car, fondamentalement, elles provoquent des bugs qui peuvent survenir au stade de la publication, lorsque les fonctionnalités isolées sont testées et fonctionnent bien. Et quand ils se sont emparés et ont frappé la sortie - hop, tout s'est effondré. Voici un exemple d'image auquel nous voulions venir.

Comment pouvez-vous aller vers elle? Il est clair qu'il existe de nombreuses options pour bien faire quelque chose. Je parlerai des principales approches et de leurs inconvénients. Bien sûr, il existe d'autres solutions.

MVP classique. Quels défis rencontrons-nous dans le MVP classique? Si nous regardons l'exemple de notre projet, nous voyons qu'il y a l'activité MVP, le fragment MVP, la vue MVP. Et il se révèle une très grande variabilité dans ce qui doit être ajouté. Dans certains cas, vous pensez que vous devez ajouter une vue et un fragment. Ensuite, il s'avère que l'ajout d'une petite fonctionnalité avec laquelle le gestionnaire vient à vous est assez difficile, car il est généralement situé dans une activité MVP distincte.
Le deuxième problème que MVP a est lié au fait que le routeur mendie. Vous voulez connecter les enfants de manière flexible et avoir une sorte d'essence pour cela. Par conséquent, les MVP arrivent généralement à une sorte de routeur self-made ou autre chose. Et l'approche guidée par la vue est un assez gros inconvénient. Dans de très nombreux modèles MVP, c'est le présentateur qui est injecté dans la vue, ce qui le rend déjà moins passif et viole l'architecture propre.

Viper, c'est mieux. Il a une telle entité comme un routeur, il est plus abstrait, et pourtant il a un certain nombre d'inconvénients. Il a toujours une logique basée sur la vue, il a la couche de présentateur requise à travers laquelle passe la logique métier, et ce n'est pas toujours vrai. La couche View est également requise, vous ne pouvez pas vous en débarrasser.
Le principal problème est que cette architecture nous est venue du monde iOS, elle doit donc être adaptée d'une certaine manière pour Android. J'ai vu qu'il y a des adaptations, et certaines d'entre elles même rien, mais il y a des inconvénients.

Il est clair que dans le monde de l'architecture, il n'y a pas de solution miracle, chaque architecture a ses avantages et ses inconvénients. Les semi-rigides ont également des inconvénients. En général, Uber a introduit cette architecture pour la plupart au niveau du concept. Ils ont pas mal de classes ouvertes, il n'y a pas d'exemples compliqués. Il existe des didacticiels simples que vous pouvez parcourir. Et lors du passage à n'importe quelle architecture, une grande quantité de refactoring suit, ce que vous devez faire, mais non seulement les RIB ont ce moins.

En quoi consiste l'architecture des semi-rigides? Elle utilise des composants Dagger. Sa classe principale, Builder, rassemble tout ce composant, qui se compose des parties suivantes: Routeur, Interacteur. Presenter (View) - un calque séparé, parfois il peut être présent, parfois absent. Dans le même temps, Presenter (View) peut être fusionné en une seule classe, ou divisé si vous avez une fausse logique de présentation.
Quoi d'autre est cool ici? Étant donné que Presenter (View) est facultatif, vous ajoutez de nouveaux écrans de la même manière que les nouvelles fonctionnalités commerciales. Votre structure est plus propre et compréhensible. L'enfant ne sait rien du parent et le parent connaît les enfants. Voyons à quoi cela ressemble comme un exemple de structure simplifiée.

Vous avez toujours une sorte de racine. Il s'agit du RIB racine. Il décide quoi inclure en lui-même, selon l'état de votre demande: il s'agit d'un état autorisé ou non autorisé. Regardons un exemple de notre application. Peut-être que vous êtes sur la commande ou non sur la commande.
À titre d'exemple, une autre fonctionnalité RIB cool. Vous pouvez créer un RIB en tant qu'écran modal, puis le connecter en principe à partir de n'importe quel RIB. Puisque le RIB ne sait rien des parents, tout parent peut fournir les dépendances nécessaires au RIB enfant.

La structure des modules peut ressembler à ceci. Pour le moment, nous pensions simplement diviser notre application en modules. Il était seul avec nous. En fait, tout est implémenté de façon assez classique. Vous avez une sorte de module commun, il peut être divisé en modules encore plus petits en fonction de vos besoins. Vous avez une sorte d'API de base, peut-être un réseau, des bases de données, etc. Et dans notre système de coordonnées, un RIB spécifique est un module séparé, il comprend tous les Common, etc., ce dont il a besoin, y compris enfants RIB.
Si certaines choses doivent être combinées entre plusieurs RIB, il existe des exemples de classes d'entités partagées qui se distinguent simplement dans des modules séparés.

Quels sont les avantages des semi-rigides? Facilité de test, isolation élevée du code, approche à activité unique, pas de douleur avec les fragments (celui qui travaille comprendra) et uniformité. Il s'agit d'une architecture multiplateforme, il existe une approche pour iOS et Android. Et si vous avez deux équipes, c'est un gros plus, car elles parleront la même langue.
Ceci est un point important. Vous voulez un peu de vie sur la mise en œuvre des RIB? Supposons que vous vous transférez des dépendances, puis vous commencez à ajouter les fonctions d'extension des héritiers et comprenez que tout cela ne vous suffit pas, vous devez l'adapter pour vous-même. Au final, il vous suffit de les prendre et de les transférer dans vos cours. Et il y a une autre façon - lorsque vous les transférez immédiatement dans vos cours, sans perdre de temps sur la première option, et adaptez-les vous-même.
Il est temps de jeter un œil au code et à son apparence.

Ils ont un plug-in pratique qui vous permet de générer les classes nécessaires pour RIB, sans perdre de temps à les créer. Il crée quatre classes principales - Builder, Interactor, Router et View, dont je parlerai plus en détail dans les diapositives suivantes. Il génère également des tests. Naturellement, il ne les écrira pas pour vous, et vous devrez les écrire vous-même, mais néanmoins, c'est plutôt sympa. Nous pensons maintenant à créer un plugin qui simplifiera la création de nouveaux modules avec RIB. Ce plugin connecterait immédiatement toutes les dépendances nécessaires, et il faudrait moins de temps pour configurer le module.

Ainsi, Builder est un composant de code de colle classique, dont la tâche principale consiste à tout assembler, à assembler un composant Dagger et View. Habituellement, View va simplement appeler le constructeur, il n'y a rien de compliqué. Dans certains cas, il peut être gonflé.

La deuxième partie, qui se trouve dans Builder, concerne les dépendances, c'est-à-dire comment l'enfant obtient des dépendances de l'extérieur.

Il possède une interface de composant parent qui définit les dépendances dont il a besoin. Ainsi, dans le générateur du composant enfant, toutes les dépendances dont il a besoin ci-dessus sont fournies.

Interactor est essentiellement la classe la plus importante de la logique métier. Seules les injections y sont autorisées. C'est pratiquement la chose la plus importante qui est testée. Il reçoit un événement de la couche UI à l'aide d'événements Stream RX. Presenter est une interface qui définit les méthodes fournies par mon événement.
Quoi d'autre est pratique pour les semi-rigides? Par le fait que sur la couche Interactor and Presenter, vous pouvez organiser l'interaction que vous aimez. Il peut s'agir de MVP, MVVM et MVVI. Ici, tout le monde est libre de choisir ce qu'il aime. Un abonnement aux événements Presenter pourrait ressembler à ceci.

Et voici à quoi pourrait ressembler le traitement de ces événements.

Routeur - une classe chargée de connecter les enfants. Il n'a pas de logique commerciale; lui-même ne fait pas se connecter les enfants. Cela rend Interactor dans un tel concept. Essentiellement, je donne ici un exemple simplifié de la façon dont cela se produit. En fait, Builder appelle simplement la méthode Build, qui collecte le RIB enfant et connecte l'enfant directement à l'aide de l'enfant attach, ainsi que l'ajout d'une vue. Le plus souvent, cette logique peut être encapsulée dans une transition distincte, vous pouvez configurer des animations - tout dépend de vos besoins.

La vue est aussi passive que possible dans cette architecture. Elle ne s'injecte rien, elle ne sait presque rien. Dans les cas les plus simples, il peut implémenter l'interface Presenter si vous n'avez aucune difficulté à la présenter. Dans les cas plus complexes, cette logique est divisée en deux classes. C'est-à-dire que vous avez une classe distincte Presenter, qui mappe simplement les données d'entreprise - par exemple, dans la vue du modèle.
Voici un exemple de la façon dont Interactor reçoit les événements d'interface utilisateur. Jetez un œil au flux Rx.

Vous ne pouvez pas simplement construire une nouvelle architecture. Lorsque vous faites cela, en particulier dans un grand projet, certaines difficultés commencent. Vous devez comprendre que nous avons un énorme projet: environ 20 activités, sinon plus, et environ 60 fragments. Toute cette logique était très fragmentée. Il fallait en quelque sorte fusionner tout cela ensemble.

Tout d'abord, vous devez tout fusionner en un seul point de navigation, d'abord créer un objet divin - un routeur d'activité, où vous gérerez également une pile de fragments, car vous aurez beaucoup d'ancien code. Personne ne vous laissera implémenter une nouvelle architecture toute la journée et arrêter une entreprise. Ce faisant, vous devrez vous lier d'amitié avec une pile de semi-rigides. Les nervures ont également naturellement une pile - elle est accessible sous le capot. Mais qu'est-ce qui est important ici? Beaucoup de code devra être complété par nous-mêmes. Uber ne prend pas en charge la rotation de l'écran, il ne s'agit donc pas tant de restaurer l'état. Par conséquent, la première chose que j'ai dû faire lorsque j'ai commencé à étudier cette architecture a été d'ajouter un héritier sur le routeur, qui prend en charge la restauration de la hiérarchie des RIB et de l'état complet de l'application.
Vous devrez prendre en charge le basculement des fonctionnalités. Aucun grand projet ne peut s'en passer. Maintenant, l'un de nos développeurs développe un concept. Si quelqu'un a regardé Mobius 2016, nous avons
parlé de Plugin Factory, ce qui vous permet de connecter et de déconnecter dynamiquement certains blocs de logique - pas nécessairement des morceaux avec des écrans. Il peut agir, par exemple, en fonction des expériences qui proviennent du serveur. Tout se fait de façon abstraite et l'interaction est simplifiée.
Le flux de travail RIB est également un concept intéressant dont vous pourriez avoir besoin. C'est lorsque vous avez plusieurs RIB qui ne se connaissent pas, sont approximativement au même niveau, mais en même temps, vous devez démarrer le processus avec les données en entrée, et à la fin, vous devez tout mettre ensemble.
Et, par exemple, des écrans modaux. Nous avons un design super personnalisé, donc il ne reste presque plus de dialogues classiques. Tout est auto-écrit, nous devons tout mettre en œuvre nous-mêmes.
Que pouvez-vous obtenir en utilisant des semi-rigides? Isolement du code, architecture simple et compréhensible, moyen facile de modularisation, suppression des fragments, approche à activité unique et commodité du développement parallèle des fonctionnalités.
Références:
-
github.com/uber/RIBs-
github.com/uber/RIBs/tree/master/android/tutorials-
habr.com/en/company/livetyping/blog/320452-
youtu.be/Q5cTT0M0YXg-
github.com/xzaleksey/Role-Playing-System-V2-
github.com/xzaleksey/DeezerSampleLe dernier lien mène à mon projet pour animaux de compagnie. Il y a six mois, j'ai commencé à développer sur des RIB pour essayer cette architecture avant de nous la présenter. Et il existe des cas plus ou moins réels qui peuvent vous aider. J'ai beaucoup expérimenté, donc il y a des choses controversées. Mais en général, vous pouvez voir comment cela fonctionne là-bas. Et peut-être qu'à partir de là, vous prendrez quelque chose pour vous.
Nous pensons également à allouer tout cela dans une bibliothèque séparée, comme nous l'avons fait à notre époque avec la bibliothèque de composants. Il y aura de telles côtes Yandex. Mais c'est dans le futur. Je t'ai tout dit, merci.