Gradle: ficheros, catálogo y dependencias
Al terminar: Distinguir implementation de api, entender namespace frente a applicationId y cómo elige Android los recursos.
La receta de Gradle ya la conoces (Los ficheros de Gradle de tu proyecto). Hoy, lo que falta: por qué una librería declarada en un módulo no llega a otro, qué son de verdad namespace y applicationId, y cómo elige Android los recursos (textos, imágenes, colores).
Es una lección de concepto: no añade código a Marcador. Temas: implementation frente a api, namespace frente a applicationId, y recursos con sus qualifiers.
¿Dónde toco para…?
- Instalar una librería: el catálogo
libs.versions.toml (Version Catalog "libs")(versión y librería) y elbuild.gradle.ktsdel módulo que la usa. - Instalar un plugin: el catálogo,
build.gradle.kts (Project: Marcador)conapply false(el plugin se declara en la raíz sin aplicarlo allí) y elbuild.gradle.ktsdel módulo, donde sí se aplica. - Que
:appuse otro módulo:build.gradle.kts (Module :app), conimplementation(project(":torneo")). - Crear un módulo nuevo: File → New → New Module…; el asistente añade el
includeasettings.gradle.kts. - Subir la versión de Kotlin o de Compose: solo
[versions]del catálogo. - Dar más memoria a Gradle: la línea
org.gradle.jvmargsdegradle.properties (Project Properties), que fija la memoria de la JVM (la máquina virtual de Java donde corre Gradle).
implementation frente a api
Cuando :app depende de :torneo, ¿ve también las librerías que usa :torneo? Depende de la palabra con que :torneo las declaró: implementation es una caja cerrada, api una caja con la tapa abierta.
// build.gradle.kts del módulo :liga (una librería inventada)
dependencies {
// api: se le pasa a quien dependa de :liga, que la ve al compilar
api(libs.kotlinx.coroutines.core)
// implementation: se compila y se empaqueta con :liga, pero NO se pasa a los demás módulos
implementation(libs.kotlinx.serialization.json)
}El classpath de compilación es la lista de librerías que el compilador ve en un módulo; quien no esté ahí da Unresolved reference, aunque la librería viaje dentro de la app. Quien dependa de :liga usa corrutinas sin declararlas, pero para kotlinx.serialization necesita su propia línea.
En Marcador, :torneo declara corrutinas y serialización con implementation, así que :app declaró sus corrutinas en El hilo principal y el ANR. Sin esa línea compilaría igualmente, porque otras librerías de AndroidX que ya tienes las declaran con api y se las pasan a :app; pero depender de que otra te las traiga es frágil: declara siempre lo que tu código usa.
Hay más palabras de este tipo, y cada una dice cuándo hace falta la librería: testImplementation (solo en los tests que corren en tu ordenador), androidTestImplementation (solo en los tests que corren en el móvil o el emulador) y debugImplementation (solo en la versión debug de la app, la que instalas mientras desarrollas, como LeakCanary).
api tiene sentido en módulos de librería como :torneo, hechos para que otros los usen. En :app existe, pero no sirve de nada: nadie depende de una aplicación. Usa implementation por defecto: con api, cambiar la librería interna obliga a recompilar a todos los dependientes y los acopla a lo que no deben conocer. api solo cuando un tipo de esa librería aparece en lo que el módulo ofrece hacia fuera.
Marcador es ese caso: :torneo ofrece StateFlow y SharedFlow, así que «de libro» llevaría api(libs.kotlinx.coroutines.core). Con implementation también funciona, porque :app declara sus propias corrutinas; si otro módulo que dependiera de :torneo quisiera esos tipos sin declararlas, habría que pasar a api, y cambiarlo es una palabra.
namespace y applicationId
En build.gradle.kts (Module :app) hay dos nombres que hoy coinciden pero son cosas distintas: uno es la dirección del código y el otro, el DNI de la app instalada.
android {
// El paquete de Kotlin del CÓDIGO. Existe en cualquier módulo de Android, también en una librería.
// El compilador lo usa para generar la clase R (los recursos) y, en la lección que viene, BuildConfig;
// el manifiesto, para completar nombres que empiezan por un punto.
namespace = "com.sebastiannarvaez.marcador"
defaultConfig {
// La identidad de la APP INSTALADA. Solo existe en el módulo que produce la app.
// Dos apps con el mismo no pueden estar instaladas a la vez; es el nombre que usan adb y las tiendas.
applicationId = "com.sebastiannarvaez.marcador"
}
}En app → manifests → AndroidManifest.xml, android:name=".MainActivity" se completa con el namespace: com.sebastiannarvaez.marcador.MainActivity. Y con la app instalada, en la Terminal de Android Studio:
adb shell pm list packages marcadorpm list packages lista las apps instaladas que contienen esa palabra. package:com.sebastiannarvaez.marcador es el applicationId, no el paquete de tu código: si cambias uno y no el otro, el código sigue en su paquete y la app tiene otro nombre. La lección siguiente lo hace a propósito.
Tres fallos que se reconocen por el síntoma
- Rojo en
libs.algojusto después de editar el catálogo: falta Sync Now, o el nombre no coincide (guion frente a punto, mayúsculas). Unresolved referencea una librería que:torneosí usa: está comoimplementationen:torneoy:appno la declara.- El módulo existe pero Gradle no lo encuentra: falta su
include(":nombre")ensettings.gradle.kts.
Para los fallos de Sync con mensaje, la caja «Si Sync sale en rojo» de Los ficheros de Gradle de tu proyecto los ordena por lo que ves.
Recuadro · Recursos y qualifiers
Marcador ya usa recursos: en el manifiesto, @string/app_name sale de app → res → values → strings.xml. Un recurso es un fichero de datos que no es código (textos, imágenes, colores). Compose dibuja con código, pero textos, colores e iconos siguen siendo recursos, y se piden con R.string.…, R.drawable.…; android.R.drawable.ic_dialog_info, el icono de la notificación de Notificaciones, es uno que trae el propio Android.
Un qualifier es un sufijo en el nombre de la carpeta que indica cuándo se usa esa alternativa. Ejemplo con dos ficheros de textos, mismo nombre y carpetas distintas:
<!-- res/values/strings.xml: la carpeta base, sin sufijo; se usa si ninguna alternativa encaja -->
<resources>
<string name="saludo">Hello</string>
</resources>
<!-- res/values-es/strings.xml: el sufijo -es es el qualifier de idioma español -->
<resources>
<string name="saludo">Hola</string>
</resources>Con el móvil en español, R.string.saludo da «Hola»; en cualquier otro idioma, «Hello». El código no cambia: Android elige el fichero según la configuración del móvil. Con imágenes pasa igual: drawable-hdpi/ es para pantallas de densidad alta (más píxeles por pulgada, es decir, más nítidas). Los ficheros alternativos se llaman igual que los de la carpeta base, y conviene tener siempre la carpeta base sin sufijo. En la vista Android no ves las carpetas del disco: las versiones de un mismo fichero salen juntas bajo un solo nodo, como las seis de ic_launcher (6).
Ejercicio · qué ve cada módulo
Reto · Ejercicio: pregúntale a Gradle qué librerías ve cada módulo
Lo de implementation se puede comprobar en tu proyecto, sin tocar ningún fichero. Pregúntale a Gradle qué librerías ve cada módulo al compilar, en la Terminal de Android Studio (abajo del todo, en la raíz del proyecto, donde está gradlew):
- Pide la lista de librerías de compilación de
:torneo. Su configuración de compilación se llamacompileClasspath. - Pide la de
:appen su versióndebug. Su configuración se llamadebugCompileClasspath. - En las dos, deja solo las líneas que mencionan
serialization-json.
Antes de ejecutar, apunta en un papel qué esperas ver en cada una: ¿sale algo? ¿En las dos? Ejecuta la primera y espera a que termine antes de lanzar la segunda: la primera vez Gradle tarda un poco.
Pista: la orden con :app:dependencies --configuration … y la barra | con grep son las de La Activity y El hilo principal y el ANR.
Ver solución
./gradlew :torneo:dependencies --configuration compileClasspath | grep "serialization-json"
./gradlew :app:dependencies --configuration debugCompileClasspath | grep "serialization-json"La primera imprime cuatro líneas (las versiones pueden variar un poco). Las | y +--- del principio solo dibujan el árbol de dependencias: una línea sangrada es una librería que cuelga de la de arriba. Ignóralas. El (c) del final marca una «restricción de versión», un aviso de Gradle sobre qué versión se permite; tampoco hace falta entenderlo ahora:
+--- org.jetbrains.kotlinx:kotlinx-serialization-json:1.11.0
| \--- org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.11.0
| | +--- org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.11.0 (c)
| | +--- org.jetbrains.kotlinx:kotlinx-serialization-json:1.11.0 (c)La segunda no imprime nada: :app depende de :torneo, pero no ve su librería de JSON, porque :torneo la declaró con implementation. (:app sí ve kotlinx-serialization-core a través de AndroidX, por eso el filtro es serialization-json y no serialization.)
Si repites la segunda orden después de Flavors, debugCompileClasspath ya no existe: la configuración se llama gratisDebugCompileClasspath (o proDebugCompileClasspath), y la salida sigue siendo vacía.
Dos preguntas para comprobar que lo has entendido:
- ¿Qué línea tendrías que añadir, y dónde, para que
:apppudiera usarJson? Enbuild.gradle.kts (Module :app),implementation(libs.kotlinx.serialization.json), como hiciste con las corrutinas. - ¿Y si una función pública de
:torneodevolviera un tipo de esa librería? Entonces:torneodebería declararla conapi, y:appla vería sin repetirla.
Qué sigue
Con esto, la lección que viene después (build types y BuildConfig) toca build.gradle.kts (Module :app) y el catálogo, y cada línea nueva tiene un sitio donde encajar. Allí los recursos vuelven: cada versión de la app (gratis, pro…) puede traer los suyos.
Comprueba lo que entendiste
5 preguntas · necesitas 4 para completar la lección
1.¿Qué fichero de Gradle dice qué módulos existen en el proyecto?
2.
:torneodeclaraimplementation(libs.kotlinx.coroutines.core)y:appdepende de:torneo. ¿Qué ocurre si:appusa corrutinas sin declararlas y ninguna otra librería de:applas trae?3.En
:torneo(un módulo de Kotlin puro) declarasapi(libs.kotlinx.serialization.json). ¿Qué cambia respecto aimplementation?4.En
AndroidManifest.xmlla pantalla se declara comoandroid:name=".MainActivity", con un punto delante. ¿Con qué se completa ese nombre?5.Un móvil está en español. Tu app tiene
res/values/strings.xmlyres/values-es/strings.xml. ¿Qué textos usa?