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 |
---|---|
|
|
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.