Multi-Modul-Projekte konfigurieren

Hintergrund



Manchmal, wenn ich zögere, mache ich die Reinigung: Ich putze den Tisch, lösche Dinge, räume den Raum auf. Tatsächlich habe ich die Umgebung in Ordnung gebracht - sie regt an und bereitet Sie auf die Arbeit vor. Beim Programmieren habe ich die gleiche Situation, nur ich bereinige das Projekt: Ich führe Refactorings durch, mache verschiedene Tools und gebe mein Bestes, um mir und meinen Kollegen das Leben zu erleichtern.



Vor einiger Zeit haben wir im Android-Team beschlossen, eines unserer Projekte - Wallet - multimodular zu gestalten. Dies führte zu einer Reihe von Vorteilen und Problemen, von denen eines die Notwendigkeit ist, jedes Modul von Grund auf neu zu konfigurieren. Natürlich können Sie die Konfiguration einfach von Modul zu Modul kopieren, aber wenn wir etwas ändern möchten, müssen wir alle Module durchlaufen.



Ich mag das nicht, das Team mag es nicht, und hier sind die Schritte, die wir unternommen haben, um unser Leben zu vereinfachen und die Wartung von Konfigurationen zu vereinfachen.







Erste Iteration - Herausziehen von Bibliotheksversionen



Tatsächlich war dies bereits in dem Projekt vor mir, und Sie kennen diesen Ansatz vielleicht. Ich sehe oft Entwickler, die es benutzen.



Der Ansatz besteht darin, die Versionen der Bibliotheken in separate globale Eigenschaften des Projekts zu verschieben und sie dann im gesamten Projekt verfügbar zu machen, um sie wiederzuverwenden. Dies erfolgt normalerweise in der Datei build.gradle auf Projektebene. Manchmal werden diese Variablen jedoch in eine separate .gradle-Datei aufgenommen und in die Hauptdatei build.gradle aufgenommen.



Höchstwahrscheinlich haben Sie solchen Code bereits im Projekt gesehen. Es steckt keine Magie darin, es ist nur eine der Gradle-Erweiterungen namens ExtraPropertiesExtension . Kurz gesagt, es ist nur Map <String, Object>, verfügbar durch ext im Projektobjektund alles andere - wie mit einem Objekt, Konfigurationsblöcken usw. - die Magie von Gradle. Beispiele:

.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




Was mir an diesem Ansatz gefällt, ist, dass er extrem einfach ist und dabei hilft, die Versionen am Laufen zu halten. Aber es hat seine Nachteile: Sie müssen sicherstellen, dass Entwickler Versionen aus diesem Satz verwenden, und dies vereinfacht die Erstellung neuer Module nicht wesentlich, da Sie noch viele Dinge kopieren müssen.



Übrigens kann ein ähnlicher Effekt mit gradle.properties anstelle von ExtraPropertiesExtension erzielt werden. Seien Sie jedoch vorsichtig : Ihre Versionen können beim Erstellen mit den -P-Flags überschrieben werden. Wenn Sie in groovigen Skripten einfach namentlich auf eine Variable verweisen, wird gradle.properties ersetzt und sie. Beispiel mit gradle.properties und 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


Zweite Iteration - project.subprojects



Meine Neugier, die an meine Unwilligkeit erinnert, den Code zu kopieren und sich mit der Konfiguration jedes Moduls zu befassen, führte mich zum nächsten Schritt: Ich erinnerte mich, dass im root build.gradle standardmäßig ein Block generiert wird - allprojects .



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


Ich ging zur Dokumentation und stellte fest, dass Sie einen Codeblock übergeben können, der dieses Projekt und alle verschachtelten Projekte konfiguriert. Dies ist jedoch nicht ganz das, was ich brauchte. Deshalb habe ich weiter gescrollt und Unterprojekte gefunden - eine Methode zum gleichzeitigen Konfigurieren aller verschachtelten Projekte. Ich musste ein paar Schecks hinzufügen, und genau das ist passiert .



Beispiel für die Konfiguration von Modulen über 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()
            }
    }
}




Jetzt können wir für jedes Modul, an das das Plugin com.android.application oder com.android.library angeschlossen ist, alles konfigurieren: Plug-In-Plugins, Plugin-Konfigurationen, Abhängigkeiten.



Ohne ein paar Probleme wäre alles in Ordnung: Wenn wir einige in Teilprojekten in einem Modul angegebene Parameter überschreiben möchten, können wir dies nicht tun, da das Modul vor dem Anwenden von Teilprojekten konfiguriert wird (dank afterEvaluate ). Wenn wir diese automatische Konfiguration nicht in einzelnen Modulen anwenden möchten, werden im Teilprojektblock viele zusätzliche Überprüfungen angezeigt. Also fing ich an weiter zu denken.



Dritte Iteration - buildSrc und Plugin



Bis zu diesem Zeitpunkt hatte ich mehrmals von buildSrc gehört und Beispiele gesehen, in denen buildSrc als Alternative zum ersten Schritt in diesem Artikel verwendet wurde. Und ich habe auch von Gradle-Plugins gehört, also habe ich angefangen, in diese Richtung zu graben. Alles stellte sich als sehr einfach heraus: Gradle verfügt über eine Dokumentation zur Entwicklung benutzerdefinierter Plugins , in die alles geschrieben ist.



Nachdem ich ein wenig verstanden hatte, erstellte ich ein Plugin , das alles konfigurieren kann, was geändert werden muss, und bei Bedarf geändert werden kann.



Plugin-Code
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
        }
    }
}




Jetzt sieht die Konfiguration des neuen Projekts so aus, als würde man das Plugin anwenden: ru'ru.yandex.money.module ' und fertig . Sie können den Android- oder Abhängigkeitsblock selbst ergänzen, Plugins hinzufügen oder anpassen. Hauptsache, das neue Modul wird in einer Zeile konfiguriert und seine Konfiguration ist immer relevant, und der Produktentwickler muss nicht mehr über das Einrichten nachdenken.



Von den Minuspunkten würde ich bemerken, dass diese Lösung zusätzliche Zeit und das Studium des Materials erfordert, aber aus meiner Sicht lohnt es sich. Wenn Sie das Plugin in Zukunft als separates Projekt verschieben möchten, würde ich nicht empfehlen, Abhängigkeiten zwischen Modulen im Plugin einzurichten .



Ein wichtiger Punkt: Wenn Sie ein Android-Gradle-Plugin unter 4.0 verwenden, sind einige Dinge in Kotlin-Skripten sehr schwierig zu erledigen - zumindest ist der Android-Block in groovigen Skripten einfacher zu konfigurieren. Es gibt ein Problem mit der Tatsache, dass einige Typen zur Kompilierungszeit nicht verfügbar sind und groovy dynamisch typisiert wird und es ihm egal ist =)



Weiter - Standalone-Plugin oder Monorepo



Natürlich ist der dritte Schritt nicht alles. Der Perfektion sind keine Grenzen gesetzt, daher gibt es Optionen für den nächsten Schritt.



Die erste Option ist das Standalone-Plugin für Gradle. Nach dem dritten Schritt ist es nicht mehr so ​​schwierig: Sie müssen ein separates Projekt erstellen, den Code dorthin übertragen und die Publikation einrichten.



Vorteile: Das Plugin kann zwischen mehreren Projekten gefummelt werden, was das Leben nicht in einem Projekt, sondern im Ökosystem vereinfacht.



Nachteile: Versionierung - Wenn Sie ein Plugin aktualisieren, müssen Sie seine Funktionalität in mehreren Projekten gleichzeitig aktualisieren und überprüfen. Dies kann einige Zeit dauern. Übrigens, meine Kollegen aus der Backend-Entwicklung haben eine hervorragende Lösung für dieses Thema. Das Schlüsselwort ist Modernizer - ein Tool, das selbst Repositorys durchläuft und Abhängigkeiten aktualisiert. Ich werde nicht lange darauf eingehen, lassen Sie sie es sich besser selbst sagen.



Monorepo - es klingt laut, aber ich habe keine Erfahrung damit, aber es gibt nur Überlegungen, dass ein Projekt wie buildSrc in mehreren anderen Projekten gleichzeitig verwendet werden kann, und dies könnte helfen, das Problem mit der Versionierung zu lösen. Wenn Sie plötzlich Erfahrung mit Monorepo haben, teilen Sie dies in den Kommentaren mit, damit ich und andere Leser etwas darüber erfahren können.



Gesamt



Führen Sie in einem neuen Projekt sofort den dritten Schritt aus - buildSrc und Plugin - es wird für alle einfacher, insbesondere da ich den Code angehängt habe . Im zweiten Schritt - project.subprojects - werden gemeinsame Module miteinander verbunden.



Wenn Sie etwas hinzufügen oder ablehnen möchten, schreiben Sie in die Kommentare oder suchen Sie mich in sozialen Netzwerken.



All Articles