Configurer des projets multi-modules

Contexte



Parfois, quand je tergiverse, je fais le ménage: nettoyer la table, sortir les choses, nettoyer la pièce. En fait, je mets de l'ordre dans l'environnement - cela dynamise et vous prépare au travail. Avec la programmation, j'ai la même situation, seulement je nettoie le projet: j'effectue des refactorisations, fais divers outils et fais de mon mieux pour me faciliter la vie et celle de mes collègues.



Il y a quelque temps, nous, dans l'équipe Android, avons décidé de rendre l'un de nos projets - Wallet - multi-modulaire. Cela a entraîné à la fois un certain nombre d'avantages et de problèmes, dont l'un est la nécessité de configurer chaque module à partir de zéro. Bien sûr, vous pouvez simplement copier la configuration d'un module à l'autre, mais si nous voulons changer quelque chose, nous devrons itérer sur tous les modules.



Je n'aime pas ça, l'équipe n'aime pas ça, et voici les étapes que nous avons prises pour simplifier nos vies et rendre les configurations plus faciles à maintenir.







Première itération - extraction des versions de bibliothèque



En fait, cela faisait déjà partie du projet avant moi, et vous connaissez peut-être cette approche. Je vois souvent des développeurs l'utiliser.



L'approche consiste à déplacer les versions des bibliothèques dans des propriétés globales distinctes du projet, puis elles deviennent disponibles tout au long du projet, ce qui permet de les réutiliser. Cela se fait généralement dans le fichier build.gradle au niveau du projet, mais parfois ces variables sont extraites dans un fichier .gradle séparé et incluses dans le fichier build.gradle principal.



Très probablement, vous avez déjà vu un tel code dans le projet. Il n'y a pas de magie dedans, c'est juste une des extensions Gradle appelée ExtraPropertiesExtension . Bref, c'est juste Map <String, Object>, disponible par ext dans l'objet projet, et tout le reste - travailler comme avec un objet, des blocs de configuration, etc. - la magie de Gradle. Exemples:

.gradle .gradle.kts
// creation
ext {
  dagger = '2.25.3'
  fabric = '1.25.4'
  mindk = 17
}

// usage
println(dagger)
println(fabric)
println(mindk)


// creation
val dagger by extra { "2.25.3" }
val fabric by extra { "1.25.4" }
val minSdk by extra { 17 }

// usage
val dagger: String by extra.properties
val fabric: String by extra.properties
val minSdk: Int by extra.properties




Ce que j'aime dans cette approche, c'est qu'elle est extrêmement simple et qu'elle aide à maintenir les versions en cours d'exécution. Mais cela a ses inconvénients: il faut s'assurer que les développeurs utilisent des versions de cet ensemble, et cela ne simplifie pas grandement la création de nouveaux modules, car il faut encore copier beaucoup de choses.



En passant, un effet similaire peut être obtenu en utilisant gradle.properties au lieu de ExtraPropertiesExtension, soyez juste prudent : vos versions peuvent être remplacées lors de la construction en utilisant les indicateurs -P, et si vous faites référence à une variable simplement par son nom dans groovy-scripts, alors gradle.properties sera remplacé et eux. Exemple avec gradle.properties et override:



// grdle.properties
overriden=2

// build.gradle
ext.dagger = 1
ext.overriden = 1

// module/build.gradle
println(rootProject.ext.dagger)   // 1
println(dagger)                   // 1

println(rootProject.ext.overriden)// 1
println(overriden)                // 2


Deuxième itération - project.subprojects



Ma curiosité, rappelée par ma réticence à copier le code et à gérer la configuration de chaque module, m'a conduit à l'étape suivante: je me suis souvenu que dans la racine build.gradle il y a un bloc qui est généré par défaut - allprojects .



allprojects {
    repositories {
        google()
        jcenter()
    }
}


Je suis allé à la documentation et j'ai trouvé que vous pouvez y passer un bloc de code qui configurera ce projet et tous les projets imbriqués. Mais ce n'est pas tout à fait ce dont j'avais besoin, alors j'ai fait défiler plus loin et j'ai trouvé des sous - projets - une méthode pour configurer tous les projets imbriqués à la fois. J'ai dû ajouter quelques chèques, et c'est ce qui s'est passé .



Exemple de configuration de modules via project.subprojects
subprojects { project ->
    afterEvaluate {
        final boolean isAndroidProject =
            (project.pluginManager.hasPlugin('com.android.application') ||
                project.pluginManager.hasPlugin('com.android.library'))

        if (isAndroidProject) {
            apply plugin: 'kotlin-android'
            apply plugin: 'kotlin-android-extensions'
            apply plugin: 'kotlin-kapt'
            
            android {
                compileSdkVersion rootProject.ext.compileSdkVersion
                
                defaultConfig {
                    minSdkVersion rootProject.ext.minSdkVersion
                    targetSdkVersion rootProject.ext.targetSdkVersion
                    
                    vectorDrawables.useSupportLibrary = true
                }

                compileOptions {
                    encoding 'UTF-8'
                    sourceCompatibility JavaVersion.VERSION_1_8
                    targetCompatibility JavaVersion.VERSION_1_8
                }

                androidExtensions {
                    experimental = true
                }
            }
        }

        dependencies {
            if (isAndroidProject) {
                // android dependencies here
            }
            
            // all subprojects dependencies here
        }

        project.tasks
            .withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile)
            .all {
                kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString()
            }
    }
}




Désormais, pour tout module avec le plugin com.android.application ou com.android.library connecté, nous pouvons tout configurer: plugins, configurations de plugins, dépendances.



Tout irait bien, sinon pour quelques problèmes: si nous voulons remplacer certains paramètres spécifiés dans les sous-projets d'un module, nous ne pourrons pas le faire, car le module est configuré avant d'appliquer les sous-projets (grâce à afterEvaluate ). Et aussi, si nous ne voulons pas appliquer cette configuration automatique dans des modules individuels, alors de nombreuses vérifications supplémentaires commenceront à apparaître dans le bloc des sous-projets. Alors j'ai commencé à réfléchir davantage.



Troisième itération - buildSrc et plugin



Jusqu'à présent, j'avais entendu parler de buildSrc à plusieurs reprises et vu des exemples dans lesquels buildSrc était utilisé comme alternative à la première étape de cet article. Et j'ai aussi entendu parler des plugins Gradle, alors j'ai commencé à creuser dans cette direction. Tout s'est avéré très simple: Gradle a une documentation pour développer des plugins personnalisés , dans laquelle tout est écrit.



Après avoir un peu compris, j'ai fait un plugin qui peut personnaliser tout ce qui doit être changé avec la possibilité de changer si nécessaire.



Code du plugin
import org.gradle.api.JavaVersion
import org.gradle.api.Plugin
import org.gradle.api.Project

class ModulePlugin implements Plugin<Project> {
    @Override
    void apply(Project target) {
        target.pluginManager.apply("com.android.library")
        target.pluginManager.apply("kotlin-android")
        target.pluginManager.apply("kotlin-android-extensions")
        target.pluginManager.apply("kotlin-kapt")

        target.android {
            compileSdkVersion Versions.sdk.compile

            defaultConfig {
                minSdkVersion Versions.sdk.min
                targetSdkVersion Versions.sdk.target

                javaCompileOptions {
                    annotationProcessorOptions {
                        arguments << ["dagger.gradle.incremental": "true"]
                    }
                }
            }

            // resources prefix: modulename_
            resourcePrefix "${target.name.replace("-", "_")}_"

            lintOptions {
                baseline "lint-baseline.xml"
            }

            compileOptions {
                encoding 'UTF-8'
                sourceCompatibility JavaVersion.VERSION_1_8
                targetCompatibility JavaVersion.VERSION_1_8
            }

            testOptions {
                unitTests {
                    returnDefaultValues true
                    includeAndroidResources true
                }
            }
        }

        target.repositories {
            google()
            mavenCentral()
            jcenter()
            
            // add other repositories here
        }

        target.dependencies {
            implementation Dependencies.dagger.dagger
            implementation Dependencies.dagger.android
            kapt Dependencies.dagger.compiler
            kapt Dependencies.dagger.androidProcessor

            testImplementation Dependencies.test.junit
            
            // add other dependencies here
        }
    }
}




Maintenant, la configuration du nouveau projet ressemble à apply plugin: ⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠'ru.yandex.money.module ' et c'est tout. Vous pouvez faire vos propres ajouts au bloc android ou dépendances, vous pouvez ajouter des plugins ou les personnaliser, mais l'essentiel est que le nouveau module soit configuré sur une seule ligne, et sa configuration est toujours pertinente et le développeur du produit n'a plus besoin de penser à le configurer.



Parmi les inconvénients, je noterais que cette solution nécessite du temps supplémentaire et une étude du matériau, mais, de mon point de vue, cela en vaut la peine. Si vous souhaitez déplacer le plugin en tant que projet séparé à l'avenir, je ne recommanderais pas de configurer des dépendances entre les modules du plugin .



Point important: si vous utilisez le plugin android gradle inférieur à 4.0, alors certaines choses sont très difficiles à faire dans les scripts kotlin - au moins le bloc android est plus facile à configurer dans les scripts groovy. Il y a un problème avec le fait que certains types ne sont pas disponibles au moment de la compilation, et groovy est typé dynamiquement, et cela n'a pas d'importance pour lui =)



Suivant - plugin autonome ou monorepo



Bien sûr, la troisième étape n'est pas tout. Il n'y a pas de limite à la perfection, il existe donc des options pour savoir où aller ensuite.



La première option est le plugin autonome pour gradle. Après la troisième étape, ce n'est plus si difficile: vous devez créer un projet séparé, y transférer le code et configurer la publication.



Avantages: le plugin peut être fouillé entre plusieurs projets, ce qui simplifiera la vie non pas dans un projet, mais dans l'écosystème.



Inconvénients: gestion des versions - lors de la mise à jour d'un plugin, vous devrez mettre à jour et vérifier ses fonctionnalités dans plusieurs projets à la fois, et cela peut prendre du temps. Au fait, mes collègues du développement backend ont une excellente solution sur ce sujet, le mot-clé est modernizer - un outil qui lui-même parcourt les référentiels et met à jour les dépendances. Je ne m'attarderai pas là-dessus pendant longtemps, ce serait mieux s'ils le disent eux-mêmes.



Monorepo - cela semble fort, mais je n'ai aucune expérience avec cela, mais il y a seulement des considérations qu'un projet, comme buildSrc, peut être utilisé dans plusieurs autres projets à la fois, et cela pourrait aider à résoudre le problème de gestion des versions. Si soudainement vous avez de l'expérience avec monorepo, partagez-la dans les commentaires afin que moi et les autres lecteurs puissent en apprendre quelque chose à ce sujet.



Total



Dans un nouveau projet, faites tout de suite la troisième étape - buildSrc et plugin - ce sera plus facile pour tout le monde, d'autant plus que j'ai joint le code . Et la deuxième étape - project.subprojects - est utilisée pour connecter des modules communs les uns aux autres.



Si vous avez quelque chose à ajouter ou à objecter, écrivez dans les commentaires ou cherchez-moi sur les réseaux sociaux.



All Articles