Configurando projetos multi-módulo

fundo



Às vezes, quando procrastino, faço a limpeza: limpo a mesa, coloco as coisas fora, arrumo o quarto. Na verdade, eu coloco o ambiente em ordem - ele energiza e prepara você para o trabalho. Com a programação, tenho a mesma situação, só que limpo o projeto: faço refatorações, faço várias ferramentas e faço o meu melhor para tornar a vida mais fácil para mim e meus colegas.



Há algum tempo, nós da equipe do Android decidimos fazer um de nossos projetos - Wallet - multimodular. Isso gerou uma série de vantagens e problemas, um dos quais é a necessidade de configurar cada módulo do zero. Claro, você pode apenas copiar a configuração de módulo para módulo, mas se quisermos mudar algo, teremos que iterar em todos os módulos.



Não gosto disso, a equipe não gosta, e aqui estão os passos que demos para simplificar nossas vidas e tornar as configurações mais fáceis de manter.







Primeira iteração - retirando versões da biblioteca



Na verdade, isso já estava no projeto antes de mim, e você deve conhecer essa abordagem. Costumo ver os desenvolvedores usando.



A abordagem é que é necessário mover as versões das bibliotecas para propriedades globais separadas do projeto, então elas se tornam disponíveis ao longo do projeto, o que ajuda a reutilizá-las. Isso geralmente é feito no arquivo build.gradle no nível do projeto, mas às vezes essas variáveis ​​são retiradas em um arquivo .gradle separado e incluídas no build.gradle principal.



Provavelmente, você já viu esse código no projeto. Não há mágica nisso, é apenas uma das extensões do Gradle chamadas ExtraPropertiesExtension . Resumindo, é apenas Map <String, Object>, acessível por ext no objeto do projeto, e tudo mais - trabalhando como se fosse um objeto, blocos de configuração e assim por diante - a magia do Gradle. Exemplos:

.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




O que eu gosto nessa abordagem é que ela é extremamente simples e ajuda a manter as versões em execução. Mas tem desvantagens: você precisa ter certeza de que os desenvolvedores usam versões deste conjunto, e isso não simplifica muito a criação de novos módulos, porque você ainda tem que copiar muitas coisas.



A propósito, um efeito semelhante pode ser alcançado usando gradle.properties em vez de ExtraPropertiesExtension, mas tome cuidado : suas versões podem ser substituídas ao construir usando os sinalizadores -P e se você se referir a uma variável simplesmente pelo nome em scripts groovy, então gradle.properties será substituído e eles. Exemplo com gradle.properties e 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


Segunda iteração - project.subprojects



Minha curiosidade, reminiscente da minha indisposição em copiar o código e lidar com a configuração de cada módulo, me levou ao próximo passo: lembrei que no build.gradle raiz existe um bloco que é gerado por default - allprojects .



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


Eu fui até a documentação e descobri que é possível passar um bloco de código que irá configurar este projeto e todos os projetos aninhados. Mas isso não é exatamente o que eu precisava, então rolei mais adiante e encontrei subprojetos - um método para configurar todos os projetos aninhados de uma vez. Tive que adicionar algumas verificações e foi isso que aconteceu .



Exemplo de configuração de módulos 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()
            }
    }
}




Agora, para qualquer módulo com o com.android.application ou com.android.library plug-in conectado, podemos configurar nada: o plugin plugins, plugins configurações, dependências.



Tudo estaria bem se não fosse por alguns problemas: se quisermos substituir alguns parâmetros especificados nos subprojetos do módulo, não poderemos fazer isso, porque o módulo é configurado antes da aplicação dos subprojetos (graças a afterEvaluate ). E também, se não quisermos aplicar esta configuração automática em módulos separados, muitas verificações adicionais começarão a aparecer no bloco de subprojetos. Então comecei a pensar mais.



Terceira iteração - buildSrc e plugin



Até este ponto, eu tinha ouvido falar sobre buildSrc várias vezes e vi exemplos em que buildSrc foi usado como uma alternativa à primeira etapa neste artigo. E também ouvi falar sobre os plug-ins do Gradle, então comecei a pesquisar nessa direção. Tudo acabou sendo muito simples: o Gradle tem documentação para o desenvolvimento de plug-ins personalizados , na qual tudo está escrito.



Depois de entender um pouco, fiz um plugin que pode configurar tudo o que precisa ser alterado com a possibilidade de alterar se necessário.



Código de 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
        }
    }
}




Agora a configuração do novo projeto se parece com o plugin de aplicação: ⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠'ru.yandex.money.module ' e é isso. Você pode fazer suas próprias adições ao bloco android ou de dependências, pode adicionar plug-ins ou personalizá-los, mas o principal é que o novo módulo é configurado em uma linha e sua configuração é sempre relevante e o desenvolvedor do produto não precisa mais pensar em configurá-lo.



Das desvantagens, eu observaria que esta solução requer tempo adicional e estudo do material, mas, do meu ponto de vista, vale a pena. Se você deseja mover o plug-in como um projeto separado no futuro, eu não recomendaria configurar dependências entre os módulos no plug-in .



Ponto importante: se você estiver usando o plug-in gradle do Android abaixo de 4.0, algumas coisas são muito difíceis de fazer em scripts kotlin - pelo menos o bloco android é mais fácil de configurar em scripts groovy. Há um problema com o fato de que alguns tipos não estão disponíveis em tempo de compilação e groovy é digitado dinamicamente, e isso não importa para ele =)



Próximo - plugin autônomo ou monorepo



Claro, a terceira etapa não é tudo. Não há limite para a perfeição, portanto, há opções para onde ir a seguir.



A primeira opção é o plug - in independente para gradle. Após a terceira etapa, não é mais tão difícil: você precisa criar um projeto separado, transferir o código para lá e configurar a publicação.



Prós: o plugin pode ser atrapalhado entre vários projetos, o que simplificará a vida não em um projeto, mas no ecossistema.



Contras: controle de versão - ao atualizar um plugin, você terá que atualizar e verificar sua funcionalidade em vários projetos ao mesmo tempo, e isso pode levar algum tempo. A propósito, meus colegas de desenvolvimento de backend têm uma excelente solução neste tópico, a palavra-chave é modernizer - uma ferramenta que percorre repositórios e atualiza dependências. Não vou me alongar sobre isso por muito tempo, seria melhor se eles próprios contassem.



Monorepo - parece alto, mas não tenho experiência com isso, mas há apenas considerações de que um projeto, como buildSrc, pode ser usado em vários outros projetos ao mesmo tempo, e isso pode ajudar a resolver o problema com o controle de versão. Se de repente você tiver experiência com o monorepo, compartilhe nos comentários para que eu e outros leitores possamos aprender algo sobre ele.



Total



Em um novo projeto, execute a terceira etapa imediatamente - buildSrc e plugin - será mais fácil para todos, especialmente porque anexei o código . E a segunda etapa - project.subprojects - é usada para conectar módulos comuns entre si.



Se você tem algo a acrescentar ou contestar, escreva nos comentários ou procure-me nas redes sociais.



All Articles