Comment faire deux applications à partir d'une seule. Expérience Tinkoff Junior

Bonjour, je m'appelle Andrey et je travaille sur les applications Tinkoff et Tinkoff Junior pour la plateforme Android. Je veux parler de la façon dont nous collectons deux applications similaires à partir d'une seule base de code.


— , ̆ 14 . , (, ), , , (, ).


.


Au début du projet, nous avons examiné différentes options pour sa mise en œuvre et pris un certain nombre de décisions. Il est immédiatement devenu évident que les deux applications (Tinkoff et Tinkoff Junior) auraient une partie importante du code commun. Nous ne voulions pas dériver de l'ancienne application, puis copier les corrections de bogues et la nouvelle fonctionnalité commune. Pour travailler avec deux applications à la fois, nous avons considéré trois options: Gradle Flavors, Git Subodules, Gradle Modules.


Saveurs Gradle


Beaucoup de nos développeurs ont déjà essayé d'utiliser Flavors, et nous pourrions également utiliser des saveurs multidimensionnelles à utiliser avec des saveurs existantes.
Cependant, les saveurs ont un défaut fatal. Android Studio considère le code uniquement comme le code de la saveur active, c'est-à-dire ce qui se trouve dans le dossier principal et dans le dossier de saveur. Le reste du code est considéré comme du texte avec des commentaires. Cela impose des restrictions sur certains outils de studio: recherche d'utilisation de code, refactoring et autres.


Sous-modules Git


Une autre option pour implémenter notre idée est d'utiliser les sous-modules de la githa: transférer le code commun vers un référentiel séparé et le connecter en tant que sous-module à deux référentiels avec le code d'une application spécifique.


Cette approche augmente la complexité de travailler avec le code source du projet. De plus, les développeurs devraient toujours travailler avec les trois référentiels pour effectuer des modifications lors du changement de l'API du module commun.


Architecture multi-modules


La dernière option consiste à passer à une architecture multi-modules. Cette approche est exempte des inconvénients des deux autres. Cependant, la transition vers une architecture multi-modules nécessite une refactorisation longue.


Au moment où nous avons commencé à travailler sur Tinkoff Junior, nous avions deux modules: un petit module API qui décrit comment travailler avec le serveur et un grand module d'application monolithique, dans lequel la majeure partie du code du projet était concentrée.


dessindessin
En conséquence, nous voulions obtenir deux modules d'application: adulte et junior et un module de base commun. Nous avons identifié deux options:


  • Mettre du code commun dans le module commun commun. Cette approche est «plus correcte», mais elle prend plus de temps. Nous avons estimé les volumes de réutilisation du code à environ 80%.
    dessin
  • Convertissez le module d'application en bibliothèque et connectez cette bibliothèque aux modules légers pour adultes et juniors . Cette option est plus rapide, mais elle apportera à Tinkoff Junior du code qui ne sera jamais exécuté.
    dessin

Nous avions du temps en réserve, et nous avons décidé de commencer le développement selon la première option (le module commun ) avec la condition de passer à l'option rapide lorsque nous manquons de temps pour le refactoring.
En fin de compte, cela s'est produit: nous avons transféré une partie du projet vers le module commun , puis transformé le module d' application restant en bibliothèque. En conséquence, nous avons maintenant la structure de projet suivante:


dessin

Nous avons des modules avec des fonctionnalités qui nous permettent de distinguer un code "adulte", général ou "enfant". Cependant, le module d' application est toujours assez grand, et maintenant environ la moitié du projet y est stocké.


Transformer l'application en bibliothèque


La documentation contient des instructions simples pour transformer une application en bibliothèque. Il contient quatre points simples et, semble-t-il, aucune difficulté ne devrait être:


  1. Ouvrir le fichier build.gradle module
  2. Supprimer applicationId de la configuration du module
  3. Au début du fichier, remplacez apply plugin: 'com.android.application' par apply plugin: 'com.android.library'
  4. Enregistrez les modifications et synchronisez le projet dans Android Studio ( Fichier> Synchroniser le projet avec les fichiers Gradle )

Cependant, la conversion a pris plusieurs jours et le diff résultant s'est révélé comme suit:


  • 183 fichiers modifiés
  • 1601 insertions (+)
  • 1920 suppressions (-)

Qu'est-ce qui a mal tourné?

Tout d'abord, dans les bibliothèques, les identifiants de ressources ne sont pas des constantes . Dans les bibliothèques, comme dans les applications, un fichier R.java est généré avec une liste d'identifiants de ressources. Et dans les bibliothèques, les valeurs d'identifiant ne sont pas constantes. Java ne vous permet pas d'activer des valeurs non constantes et tous les commutateurs doivent être remplacés par if-else.


 // Application int id = view.getId(); switch(id) { case R.id.button1: action1(); break; case R.id.button2: action2(); break; } // Library int id = view.getId(); if (id == R.id.button1) { action1(); } else if (id == R.id.button2) { action2(); } 

Ensuite, nous sommes tombés sur une collision de paquets.
Supposons que vous ayez une bibliothèque qui contient package = com.example et que l'application avec package = com.example.app dépend de cette bibliothèque. Ensuite, la classe com.example.R sera générée dans la bibliothèque et com.example.app.R , respectivement , dans l'application. Créons maintenant l' activité com.example.MainActivity dans l'application, dans laquelle nous allons essayer d'accéder à la classe R. Sans importation explicite, la classe R de la bibliothèque sera utilisée, dans laquelle les ressources d'application ne sont pas spécifiées, mais uniquement les ressources de la bibliothèque. Cependant, Android Studio ne met pas en évidence l'erreur, et lorsque vous essayez de passer du code à une ressource, tout ira bien.


Poignard


Nous utilisons Dagger comme cadre d'injection de dépendances.
Dans chaque module contenant l'activité, des fragments et des services, nous avons les interfaces habituelles qui décrivent les méthodes d' injection pour ces entités. Dans les modules d'application ( adulte et junor ), les interfaces des composants poignards héritent de ces interfaces. Dans les modules, nous apportons les composants aux interfaces nécessaires à ce module.


Multibindings


Le développement de notre projet est grandement simplifié par l'utilisation de liaisons multiples.
Dans l'un des modules communs, nous définissons une interface. Dans chaque module d'application ( adulte , junior ), nous décrivons la mise en œuvre de cette interface. En utilisant l'annotation @Binds , nous indiquons au poignard que chaque fois au lieu d'une interface, il est nécessaire d'injecter son implémentation spécifique pour une application enfant ou adulte. Nous collectons également souvent une collection d'implémentations d'interface (Set ou Map), et ces implémentations sont décrites dans différents modules d'application.


Saveurs


À différentes fins, nous collectons plusieurs options d'application. Les saveurs décrites dans le module de base doivent également être décrites dans les modules dépendants. De plus, pour qu'Android Studio fonctionne correctement, il est nécessaire que des options d'assemblage compatibles soient sélectionnées dans tous les modules du projet.


Conclusions


En peu de temps, nous avons implémenté une nouvelle application. Maintenant, nous expédions la nouvelle fonctionnalité dans deux applications, en l'écrivant une fois.


Dans le même temps, nous avons passé un certain temps à refactoriser, en réduisant simultanément la dette technique, et nous sommes passés à une architecture multi-modules. En cours de route, nous avons rencontré des restrictions du SDK Android et d'Android Studio, que nous avons réussi à gérer.

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


All Articles