Mengonfigurasi proyek multi-modul

Latar Belakang



Kadang-kadang, ketika saya menunda-nunda, saya melakukan pembersihan: membersihkan meja, meletakkan barang-barang, membersihkan kamar. Sebenarnya, saya menertibkan lingkungan - itu memberi energi dan membuat Anda siap untuk bekerja. Dengan pemrograman, saya memiliki situasi yang sama, hanya saya yang membersihkan proyek: Saya melakukan refactorings, membuat berbagai alat, dan melakukan yang terbaik untuk membuat hidup lebih mudah bagi saya dan kolega saya.



Beberapa waktu lalu, kami di tim Android memutuskan untuk membuat salah satu proyek kami - Wallet - multi-modular. Hal ini menyebabkan sejumlah keuntungan dan masalah, salah satunya adalah kebutuhan untuk mengkonfigurasi setiap modul dari awal. Tentu saja, Anda dapat menyalin konfigurasi dari modul ke modul, tetapi jika kita ingin mengubah sesuatu, kita harus mengulang semua modul.



Saya tidak suka ini, tim tidak menyukainya, dan berikut adalah langkah-langkah yang telah kami ambil untuk menyederhanakan hidup kami dan membuat konfigurasi lebih mudah dipertahankan.







Iterasi pertama - mengeluarkan versi pustaka



Sebenarnya, ini sudah ada dalam proyek sebelum saya, dan Anda mungkin tahu pendekatan ini. Saya sering melihat developer menggunakannya.



Pendekatannya adalah perlu untuk memindahkan versi pustaka ke properti global proyek yang terpisah, kemudian mereka tersedia di seluruh proyek, yang membantu untuk menggunakannya kembali. Ini biasanya dilakukan dalam file build.gradle di tingkat proyek, tetapi terkadang variabel ini dibawa ke file .gradle terpisah dan disertakan dalam build.gradle utama.



Kemungkinan besar, Anda telah melihat kode seperti itu di proyek. Tidak ada keajaiban di dalamnya, ini hanya salah satu ekstensi Gradle yang disebut ExtraPropertiesExtension . Singkatnya, ini hanya Map <String, Object>, dapat diakses oleh ext di objek proyek, dan yang lainnya - bekerja seolah-olah dengan sebuah objek, blok konfigurasi, dan sebagainya - keajaiban Gradle. Contoh:

.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




Yang saya suka dari pendekatan ini adalah sangat sederhana dan membantu menjaga versi tetap berjalan. Tetapi ada kekurangannya: Anda perlu memastikan bahwa pengembang menggunakan versi dari set ini, dan ini tidak terlalu menyederhanakan pembuatan modul baru, karena Anda masih harus menyalin banyak hal.



Ngomong-ngomong, efek serupa dapat dicapai dengan menggunakan gradle.properties daripada ExtraPropertiesExtension, berhati-hatilah : versi Anda dapat diganti saat membuat menggunakan tanda -P, dan jika Anda merujuk ke variabel hanya dengan nama di skrip groovy, maka gradle.properties akan diganti dan mereka. Contoh dengan gradle.properties dan 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


Iterasi kedua - project.subprojects



Rasa ingin tahu saya, yang mengingatkan pada keengganan saya untuk menyalin kode dan menangani konfigurasi setiap modul, membawa saya ke langkah berikutnya: Saya ingat bahwa di root build.gradle ada blok yang dihasilkan secara default - allprojects .



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


Saya pergi ke dokumentasi dan menemukan bahwa adalah mungkin untuk melewatkan satu blok kode ke dalamnya yang akan mengkonfigurasi proyek ini dan semua proyek bersarang. Tapi ini bukan yang saya butuhkan, jadi saya menggulir lebih jauh dan menemukan subproyek - metode untuk mengkonfigurasi semua proyek bersarang sekaligus. Saya harus menambahkan beberapa cek, dan inilah yang terjadi .



Contoh konfigurasi modul melalui 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()
            }
    }
}




Sekarang untuk setiap modul dengan com.android.application atau com.android.library Plugin terhubung, kita dapat mengkonfigurasi apa-apa: Plugin plugin, plugin ini konfigurasi, dependensi.



Semuanya akan baik-baik saja, jika bukan karena beberapa masalah: jika kita ingin mengganti beberapa parameter yang ditentukan dalam subproyek dalam modul, maka kita tidak akan dapat melakukan ini, karena modul dikonfigurasi sebelum subproyek diterapkan (terima kasih kepada afterEvaluate ). Dan juga, jika kita tidak ingin menerapkan konfigurasi otomatis ini di modul individu, maka banyak pemeriksaan tambahan akan mulai muncul di blok subproyek. Jadi saya mulai berpikir lebih jauh.



Iterasi ketiga - buildSrc dan plugin



Sampai saat ini, saya telah mendengar tentang buildSrc beberapa kali dan melihat contoh di mana buildSrc digunakan sebagai alternatif dari langkah pertama dalam artikel ini. Dan saya juga mendengar tentang plugin gradle, jadi saya mulai menggali ke arah ini. Semuanya ternyata sangat sederhana: Gradle memiliki dokumentasi untuk mengembangkan plugin khusus , di mana semuanya sudah tertulis.



Setelah memahami sedikit, saya membuat sebuah plugin yang dapat menyesuaikan segala sesuatu yang perlu diubah dengan kemampuan untuk diubah jika perlu.



Kode 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
        }
    }
}




Sekarang konfigurasi proyek baru terlihat seperti menerapkan plugin: ⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠'ru.yandex.money.module ' dan hanya itu. Anda dapat membuat tambahan sendiri ke blok android atau dependensi, Anda dapat menambahkan plugin atau menyesuaikannya, tetapi yang utama adalah modul baru dikonfigurasi dalam satu baris, dan konfigurasinya selalu relevan dan pengembang produk tidak perlu lagi memikirkan untuk menyiapkannya.



Dari kekurangannya, saya akan mencatat bahwa solusi ini membutuhkan waktu tambahan dan studi materi, tetapi, dari sudut pandang saya, itu sangat berharga. Jika Anda ingin memindahkan plugin sebagai proyek terpisah di masa mendatang, maka saya tidak akan merekomendasikan pengaturan dependensi antar modul di plugin .



Poin penting: jika Anda menggunakan plugin gradle android di bawah 4.0, maka beberapa hal sangat sulit dilakukan di skrip kotlin - setidaknya blok android lebih mudah dikonfigurasi dalam skrip groovy. Ada masalah dengan fakta bahwa beberapa tipe tidak tersedia pada waktu kompilasi, dan groovy diketik secara dinamis, dan tidak masalah baginya =)



Berikutnya - plugin atau monorepo mandiri



Tentu saja, langkah ketiga tidak semuanya. Tidak ada batasan untuk kesempurnaan, jadi ada pilihan ke mana harus pergi selanjutnya.



Opsi pertama adalah plugin mandiri untuk gradle. Setelah langkah ketiga, tidak lagi begitu sulit: Anda perlu membuat proyek terpisah, mentransfer kode di sana, dan menyiapkan publikasi.



Kelebihan: plugin dapat digunakan di antara beberapa proyek, yang akan menyederhanakan hidup tidak dalam satu proyek, tetapi dalam ekosistem.



Kekurangan: pembuatan versi - saat memperbarui plugin, Anda harus memperbarui dan memeriksa fungsinya di beberapa proyek sekaligus, dan ini bisa memakan waktu. Omong-omong, kolega saya dari pengembangan backend memiliki solusi yang sangat baik untuk topik ini, kata kuncinya adalah modernizer - alat yang berjalan dengan sendirinya melalui repositori dan memperbarui dependensi. Saya tidak akan memikirkan ini untuk waktu yang lama, akan lebih baik jika mereka sendiri yang memberi tahu.



Monorepo - kedengarannya keras, tetapi saya tidak memiliki pengalaman dengannya, tetapi hanya ada pertimbangan bahwa satu proyek, seperti buildSrc, dapat digunakan di beberapa proyek lain sekaligus, dan ini dapat membantu menyelesaikan masalah dengan pembuatan versi. Jika tiba-tiba Anda memiliki pengalaman dengan monorepo, maka bagikan di komentar agar saya dan pembaca lain dapat mempelajarinya.



Total



Dalam proyek baru, lakukan langkah ketiga segera - buildSrc dan plugin - akan lebih mudah bagi semua orang, terutama karena saya telah melampirkan kodenya . Dan langkah kedua - project.subprojects - digunakan untuk menghubungkan modul umum satu sama lain.



Jika Anda memiliki sesuatu untuk ditambahkan atau ditolak, tulis di komentar atau cari saya di jejaring sosial.



All Articles