DEV Community

Rodrigo Sicarelli
Rodrigo Sicarelli

Posted on • Edited on

Android Plataforma - Parte 10: Customização dos módulos

🌱 Branch: 10-11/customizing-android-options

🔗 Repositório: github.com/rsicarelli/kotlin-gradle-android-platform

⬅️ Artigo Anterior: Parte 9: Unificando a Application e Library extensions com a Common Extension

➡️ Próximo Artigo: Parte 11: Criando uma DSL para customizar as novas opções


No último artigo, exploramos o CommonsExtension para eliminar duplicidades em nossas configurações.

Agora, vamos discutir situações em que são necessárias modificações no comportamento e como enriquecer nossa plataforma com uma DSL customizada para a construção de um AndroidOptions.

Ainda enfrentamos duplicidade ao definir nossos buildTypes, além de não estarmos configurando o Proguard corretamente para as nossas biblotecas.

Mas, antes de resolver essa questão, vale a pena entender como cada módulo pode ter configurações específicas.


Módulos diferentes, configurações diferentes.

Em uma aplicação real, é comum que diferentes módulos demandem certa flexibilidade em relação à plataforma.

Por exemplo, talvez um módulo necessite de um build type adicional, modificar as regras do "resource packing" para excluir determinados arquivos, ou até usar um namespace diferente.

Então, como podemos incorporar essa flexibilidade à nossa plataforma?

Introduzindo o conceito de Options

Cada ajuste em nossa plataforma pode ser adaptado a partir de um modelo ou opções, permitindo maior controle sobre determinado módulo.

A proposta é criar um modelo que especifique quais opções serão aplicadas para cada módulo.

Image description

sealed class AndroidOptions( open val namespace: String, open val compileSdk: Int, open val minSdk: Int, open val useVectorDrawables: Boolean, open val javaVersion: JavaVersion, open val composeOptions: ComposeOptions, open val packagingOptions: PackagingOptions, open val proguardOptions: ProguardOptions, open val buildTypes: List<AndroidBuildType>, ) { data class AndroidAppOptions( val applicationId: String, val targetSdk: Int, val versionCode: Int, val versionName: String, override val proguardOptions: ProguardOptions, override val namespace: String, override val compileSdk: Int, override val minSdk: Int, override val useVectorDrawables: Boolean, override val javaVersion: JavaVersion, override val composeOptions: ComposeOptions, override val packagingOptions: PackagingOptions, override val buildTypes: List<AndroidBuildType>, ) : AndroidOptions( namespace = namespace, compileSdk = compileSdk, minSdk = minSdk, useVectorDrawables = useVectorDrawables, javaVersion = javaVersion, composeOptions = composeOptions, packagingOptions = packagingOptions, proguardOptions = proguardOptions, buildTypes = buildTypes ) data class AndroidLibraryOptions( override val proguardOptions: ProguardOptions, override val namespace: String, override val compileSdk: Int, override val minSdk: Int, override val useVectorDrawables: Boolean, override val javaVersion: JavaVersion, override val composeOptions: ComposeOptions, override val packagingOptions: PackagingOptions, override val buildTypes: List<AndroidBuildType>, ) : AndroidOptions( namespace = namespace, compileSdk = compileSdk, minSdk = minSdk, useVectorDrawables = useVectorDrawables, javaVersion = javaVersion, composeOptions = composeOptions, packagingOptions = packagingOptions, proguardOptions = proguardOptions, buildTypes = buildTypes ) } data class ProguardOptions( val fileName: String, val applyWithOptimizedVersion: Boolean = true, ) data class ComposeOptions( val enabled: Boolean = true, ) data class PackagingOptions( val excludes: String = "/META-INF/{AL2.0,LGPL2.1}", ) interface AndroidBuildType { val name: String val isMinifyEnabled: Boolean val shrinkResources: Boolean val versionNameSuffix: String? val isDebuggable: Boolean val multidex: Boolean } object ReleaseBuildType : AndroidBuildType { override val name: String = "release" override val isMinifyEnabled: Boolean = true override val shrinkResources: Boolean = true override val versionNameSuffix: String? = null override val isDebuggable: Boolean = false override val multidex: Boolean = false } object DebugBuildType : AndroidBuildType { override val name: String = "debug" override val isMinifyEnabled: Boolean = false override val shrinkResources: Boolean = false override val versionNameSuffix: String = "debug" override val isDebuggable: Boolean = true override val multidex: Boolean = false } 
Enter fullscreen mode Exit fullscreen mode

A partir desse modelo, conseguimos:

  • Estabelecer opções comuns entre diferentes tipos de módulos Android usando a sealed class AndroidOptions.
  • Especificar opções para o app com o AndroidAppOptions.
  • Delimitar opções para uma biblioteca usando o AndroidLibraryOptions.
  • Ter maior adaptabilidade para definir as opções do Proguard.
  • Tornar nossa plataforma agnóstica, facilitando a integração com outros projetos que tenham applicationId distintos, entre outros.

Refatorando com AndroidOptions

1 - Crie um arquivo nomeado AndroidOptions.kt na raiz do módulo build-logic e mova o conteúdo anterior para este arquivo.

Traga todo o conteúdo acima para esse arquivo.

2 - Atualize a função applyAndroidCommon() trazendo o AndroidOptions como argumento.

Atualize a função para utilizarmos os valores definidos pelo modelo:

private fun Project.applyAndroidCommon(androidOptions: AndroidOptions) = with(commonExtension) { namespace = androidOptions.namespace compileSdk = androidOptions.compileSdk defaultConfig { minSdk = androidOptions.minSdk vectorDrawables { useSupportLibrary = androidOptions.useVectorDrawables } } compileOptions { sourceCompatibility = androidOptions.javaVersion targetCompatibility = androidOptions.javaVersion } applyKotlinOptions() androidOptions.composeOptions.takeIf(ComposeOptions::enabled) ?.let { buildFeatures { compose = true } composeOptions { kotlinCompilerExtensionVersion = libs.version("composeKotlinCompilerExtension") } } packaging { resources { excludes += androidOptions.packagingOptions.excludes } } } 
Enter fullscreen mode Exit fullscreen mode

3 - Atualize nossas funções applyAndroidApp() e
applyAndroidLibrary() para receber e aplicar as opções do modelo, assim como invocar nossa applyAndroidCommon()

internal fun Project.applyAndroidApp(androidAppOptions: AndroidAppOptions) { applyAndroidCommon(androidAppOptions) extensions.configure<ApplicationExtension> { defaultConfig { applicationId = androidAppOptions.applicationId targetSdk = androidAppOptions.targetSdk versionCode = androidAppOptions.versionCode versionName = androidAppOptions.versionName } } } 
Enter fullscreen mode Exit fullscreen mode
internal fun Project.applyAndroidLibrary(androidLibraryOptions: AndroidLibraryOptions) { applyAndroidCommon(androidLibraryOptions) extensions.configure<LibraryExtension> { } } 
Enter fullscreen mode Exit fullscreen mode

4 - Vamos criar uma DSL para definir as configurações do Proguard.

A ideia dessa função é delegar a função consume para quem invoca, e deixar aplicar configurações específicas para cada tipo de módulo

private fun <T> Project.setProguardFiles( config: T, proguardOptions: ProguardOptions, consume: T.(Array<Any>) -> Unit, ) { if (proguardOptions.applyWithOptimizedVersion) { config.consume( arrayOf( getDefaultProguardFile("proguard-android-optimize.txt", layout.buildDirectory), proguardOptions.fileName ) ) } else { config.consume(arrayOf(proguardOptions.fileName)) } } 
Enter fullscreen mode Exit fullscreen mode

5 - Atualize as funções applyAndroidApp() e applyAndroidLibrary(), definindo o proguard dentro do bloco defaultConfig { }. Aqui, você terá acesso às funções proguardFiles e consumerProguardFiles:

internal fun Project.applyAndroidApp(androidAppOptions: AndroidAppOptions) { applyAndroidCommon(androidAppOptions) extensions.configure<ApplicationExtension> { defaultConfig { .. setProguardFiles( config = this, proguardOptions = androidAppOptions.proguardOptions, consume = { proguardFiles(*it) } ) } } } 
Enter fullscreen mode Exit fullscreen mode
internal fun Project.applyAndroidLibrary(androidLibraryOptions: AndroidLibraryOptions) { applyAndroidCommon(androidLibraryOptions) extensions.configure<LibraryExtension> { defaultConfig { setProguardFiles( config = this, proguardOptions = androidLibraryOptions.proguardOptions, consume = { consumerProguardFiles(*it) } ) } } } 
Enter fullscreen mode Exit fullscreen mode

6 - Em seguida, configure os buildTypes a partir da List<ApplicationBuildType>:

Para a ApplicationExtension:

private fun ApplicationExtension.setAppBuildTypes(options: AndroidAppOptions) { fun ApplicationBuildType.applyFrom(androidBuildType: AndroidBuildType) { isDebuggable = androidBuildType.isDebuggable isMinifyEnabled = androidBuildType.isMinifyEnabled isShrinkResources = androidBuildType.shrinkResources multiDexEnabled = androidBuildType.multidex versionNameSuffix = androidBuildType.versionNameSuffix } buildTypes { options.buildTypes.forEach { androidBuildType -> when (androidBuildType) { DebugBuildType -> debug { applyFrom(androidBuildType) } ReleaseBuildType -> release { applyFrom(androidBuildType) } else -> create(androidBuildType.name) { applyFrom(androidBuildType) } } } } } 
Enter fullscreen mode Exit fullscreen mode

Para a LibraryExtension:

private fun LibraryExtension.setLibraryBuildTypes(options: AndroidLibraryOptions) { fun LibraryBuildType.applyFrom(androidBuildType: AndroidBuildType) { isMinifyEnabled = androidBuildType.isMinifyEnabled multiDexEnabled = androidBuildType.multidex } buildTypes { options.buildTypes.forEach { androidBuildType -> when (androidBuildType) { DebugBuildType -> debug { applyFrom(androidBuildType) } ReleaseBuildType -> release { applyFrom(androidBuildType) } else -> create(androidBuildType.name) { applyFrom(androidBuildType) } } } } } 
Enter fullscreen mode Exit fullscreen mode

7 - Por fim, integre todos os componentes:

internal fun Project.applyAndroidApp(androidAppOptions: AndroidAppOptions) { applyAndroidCommon(androidAppOptions) extensions.configure<ApplicationExtension> { defaultConfig { applicationId = androidAppOptions.applicationId targetSdk = androidAppOptions.targetSdk versionCode = androidAppOptions.versionCode versionName = androidAppOptions.versionName setProguardFiles( config = this, proguardOptions = androidAppOptions.proguardOptions, consume = { proguardFiles(*it) } ) } setAppBuildTypes(androidAppOptions) } } 
Enter fullscreen mode Exit fullscreen mode
internal fun Project.applyAndroidLibrary(androidLibraryOptions: AndroidLibraryOptions) { applyAndroidCommon(androidLibraryOptions) extensions.configure<LibraryExtension> { defaultConfig { setProguardFiles( config = this, proguardOptions = androidLibraryOptions.proguardOptions, consume = { consumerProguardFiles(*it) } ) } setLibraryBuildTypes(androidLibraryOptions) } } 
Enter fullscreen mode Exit fullscreen mode

Sucesso!

Com essa adaptação, tornamos nossos ajustes mais flexíveis, podendo, por exemplo, habilitar o Compose em um módulo específico.

No entanto, ainda há desafios pela frente.

Precisamos encontrar uma maneira de permitir que os módulos definam esses parâmetros.

Uma opção seria aceitar um modelo predefinido, mas no próximo artigo, construiremos juntos uma DSL, buscando uma abordagem mais fluida e idiomática no Kotlin, sem a necessidade de criar objetos em módulos individuais.

Top comments (0)