Cuando comenzamos nuestro viaje en la programación Android, solo queremos crear nuevas cosas geniales, bonitas y robustas. Llegamos a un punto en el que comenzamos a aprender sobre patrones de diseño, arquitectura y mejores prácticas, pero usualmente dedicamos menos tiempo a aprender sobre Gradle. Para mi, este fue uno de los puntos pendientes en mi carrera como desarrollador. Bueno… ya no, y si estás en la misma situación, espero que esto empiece a cambiar ahora.
Cada vez que comencé un nuevo proyecto o tenía que hacer cambios a uno existente relacionado a gradle, lo que hacía era abrir un proyecto viejo o ir a StackOverflow y copiar-pegar un pequeño snippet sin entender completamente lo que ese pedazo de código estaba haciendo. A veces, esto era cuestión de ensayo y error. También el hecho de que necesitaba usar Groovy (que no me gusta mucho) lo hizo más complicado.
En este artículo te mostrare que Gradle no es tan terrible como puedes pensar. No necesitas saber Groovy y hay una manera mas simple de trabajar con el y así finalmente Domar a la Bestia.
Lifecycle (Ciclo de vida)
Lo primero que debes saber es que cada vez que iniciamos un build, gradle pasará por tres estapas de su ciclo de vida, y en cada uno de esas fases ocurren varias cosas.
Inicialización
Esta es la primera etapa del ciclo de vida que se ejecuta. Lo que hace es basicamente “preparar” el proyecto para el build. Para esto, toma los llamados init scripts, ubicados en la carpeta .gradle/init.d
(si existe), pasando por ellos en orden alfabético. En estos scripts podemos añadir configuraciones iniciales, como establecer propiedades o un ambiente específico en el que vamos a ejecutar nuestro build, como el servidor de Desarrollo or Integración Continua (CI). Tambien usa el archivo settings.gradle
para determinar los proyectos que serán parte del build, creando una instancia para cada proyecto.
Configuración
Durante esta estapa, se ejecuta el script de compilación (build) de todos los proyectos que se crearon anteriormente. Por esta razón, cada proyecto necesita un archivo build.gradle
en el que pueda configurarse, añadir tareas (task), dependencias y más.
Ejecución
Gradle determina el subconjunto de tareas, creadas y configuradas durante la etapa de Configuración, que serán ejecutadas. El subconjunto está determinado por los nombres de tareas pasados como argumentos al comando gradle
y el directorio actual. Gradle luego ejecuta cada una de las tareas seleccionas.
Interfaces
Ahora que estamos familiarizados con el ciclo de vida, veamos de dónde vienen todos esos métodos y propiedades que tenemos en nuestros scripts y dónde podemos encontrar lo que necesitemos.
Si has usado Gradle en un proyecto Java o Android, habrás notado algunos archivos con la extensión .gradle
. Anteriormente mencionamos dos de ellos, settings.gradle
y build.gradle
. Como parte de los scripts de inicialización también tenemos el init.gradle
o cualquier otro que esté en la misma carpeta.
Cada uno de estos extiende de una Interfaz. En algunos casos, más de una. Es por esto que podemos acceder a sus métodos y propiedades, y que probablemente antes de saber que eran interfaces, pensabas que era magia, porque yo si 😄. Pues no temáis!
Lo que debes saber es que todos los scripts que tenemos en nuestro proyecto, implementan la interfaz Script. Si revisamos su definición podemos ver que tiene un método público llamado getBuildscript(). Te suena familiar? Correcto, este lo encontramos al principio del build.gradle de nuestra aplicación.
buildscript {
repositories {
...
}
dependencies {
...
}
}
Este método retorna un ScriptHandler
el cual expone getRepositories()
y getDependencies()
. Nota que en Groovy, similar a Kotlin, no necesitamos utilizar el getter con su nombre completo, sino que podemos usar la “propiedad de sintaxis” como se muestra arriba. Esto ya no parece magia para nada!
Podemos revisar la documentación todo lo que queramos, pero lo que encontramos generalmente en un proyecto son los archivos build.gradle
que implementan la interfaz Project y el settings.gradle
que implementa… (adivinaste) la interfaz Settings. También existe un caso menos común en el mundo Android y es el init.gradle
que implementa Gradle.
Propiedades
Quizás has notado que tambien tenemos archivos con la extension .properties
, como lo es el gradle.properties
. En este archivo podemos definir nuestras propiedades como un par key-value.
some_custom_property_key=some_custom_property_value
Podemos acceder a esta propiedades desde nuestro script de la siguiente forma:
println some_custom_property_key
Pero tambien podemos definirlas en un archivo script. De hecho, muchos de ustedes podrían estar familiarizados con el manejo de dependencias de sus proyectos desde un lugar unico usando “ext”. Si nunca habias visto esto, déjame mostrarte lo que es.
El ojo agudo puede haber notado que algunas de las interfaces mencionadas anteriormente también extienden desde ExtensionAware
que exponen una [ExtraPropertiesExtension](ExtraPropertiesExtension (Gradle API 6.5)), el cual, segun la documentación “siempre está presente en el contenedor con el nombre “ext”. Esto nos permite añadir nuestras propiedades directamente de la siguiente manera.
project.ext.custom_property = "some_value"
// otra forma de hacer lo mismo
project.ext {
custom_property = "some_value"
another_property = "another_value"
}
Observa que usé project.ext
. En este caso, project
es nuestro “objeto delegado” que resolverá cualquier propiedad o método que nuestro scope actual no conozca y también expone todas sus propiedades para usarlas en nuestro script. Es por esto que en este scope podemos usar ext
directamente.
El objeto delegado será diferente para cada tipo de script. La siguiente tabla muestra el tipo para cada uno.
Type of Script | Delegate |
---|---|
Build | Project |
Init | Gradle |
Settings | Settings |
Plugins
Gradle tambien nos permite extender sus capacidades con plugins. Un plugin empaqueta funcionalidades que pueden ser reutilizadas en muchos proyectos, puede añadir nuevos DSL, ser configurado, añadir tareas y más.
Por ejemplo en un proyecto de aplicación Android, en el archivo app/build.gradle
podemos aplicar algunos plugins con apply plugin: 'my-plugin'
. Uno de ellos es 'com.android.application'
, el cual viene del Android Gradle Plugin. Es por esto que podemos añadir el bloque android
y configurar nuestro proyecto.
android {
defaultConfig {
minSdkVersion 24
targetSdkVersion 29
...
}
signingConfigs { }
buildTypes { }
...
}
Existen muchos plugins ahí afuera que podemos aplicar a nuestro proyecto y tambien podemos crear los nuestros! Pero dejemos eso para otro artículo 😉.
Tareas (Tasks)
Algo que encuentro muy poderoso y que me tome un tiempo entender es el uso de Tasks. Por defecto, gradle registra una serie de tareas como build, assemble, dependencies, test y muchas más. En la etapa de configuración, gradle pasa por cada script, crea una lista de tareas y luego determina el orden de ejecución, generando así un taskGraph. Puedos consultar este taskGraph e imprimirlo en la consola. Para esto utilizaremos el método getTaskGraph()
.
gradle.taskGraph.whenReady { graph ->
logger.info ">>> taskGraph: ${graph.allTasks}"
}
Cuando ejecutemos un build, veremos el taskGraph en la consola.
./gradlew build -i | grep taskGraph
>>> taskGraph: [task ':app:preBuild', task ':app:preDebugBuild', ... ]
En este caso el “objeto delegado” es project
, pero podemos saltarlo en este caso y utilizar gradle
directamente. Si no especificamos gradle, nos arrojará el siguiente error:
Could not get unknown property ’taskGraph’ for root project ‘MyProject’ of type org.gradle.api.Project.
Es posible obtener una lista de todas las tareas y ejecutar operaciones. Por ejemplo, si queremos cambiar la información que se imprime cuando corremos los tests, podemos obtener las tareas con getTasks()
, filtrar las que nos interesan por tipo y aplicar configuraciones nuevas.
tasks.withType(Test) {
testLogging {
events "skipped", "failed", "passed"
}
}
Otro ejemplo sería cambiar la versión de Java que usamos para compilar el proyecto. Podemos hacerlo chequeando los plugins aplicados. Para modulos android, podemos cambiarlo de esta manera.
// Application
plugins.withType(com.android.build.gradle.AppPlugin)
.configureEach { plugin ->
plugin.extension.compileOptions {
sourceCompatibility = "$java_version"
targetCompatibility = "$java_version"
}
}
// Android library
plugins.withType(com.android.build.gradle.LibraryPlugin)
.configureEach { plugin ->
plugin.extension.compileOptions {
sourceCompatibility = "$java_version"
targetCompatibility = "$java_version"
}
}
También podemos crear nuestras propias tareas, establecer el órden y dependencias. Vale la pena mencionar que gradle no permite dependencias circulares, por lo que si definimos cuatro tareas: A, B, C y D, donde B depende de A, C depende de B, D depende de B y C, y A depende de C, no nos permitirá hacerlo y detectará que hay una dependencia circular (A y C). También es lo suficientemente inteligente para saber que hay dos tareas dependiendo de una (C y D dependen de B), así que ejectura B sólo una vez.
task doA { }
task doB(dependsOn: 'doA') { }
task doC(dependsOn: 'doB') { }
task doD(dependsOn: ['doB', 'doC']) { } // este task depende de dos tasks []
Existen otras maneras de crear tareas e incluso podemos usar Java o Kotlin para ello. Si quieres saber más sobre esto, te recomiendo revisar este documento en el que encontrarás algunos ejemplos.
Conclusión
Gracias por llegar hasta el final. Como mencionó al principio, Gradle no es tan terrible y toda esa “magia” que vemos en nuestros scripts no es tan complicada. La documentación lo explica bastante bien así que no debemos tener miedo a crear nuestras propias soluciones.
Ahora que conocemos el ciclo de vida, las interfaces base, cómo podemos acceder a métodos y properties que son expuestas por las interfaces, qué son plugins, tareas y como podemos crear las nuestras, estamos preparados para enfrentar problemas con mayor confianza. No dudes en probar cosas nuevas. Crea plugins que añadan valor a tu proyecto y mejora como desarrollador, porque desde este momento, has domado a la bestia.