Saltar al contenido
K
Android: la plataformaConcepto25 min Básico

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 el build.gradle.kts del módulo que la usa.
  • Instalar un plugin: el catálogo, build.gradle.kts (Project: Marcador) con apply false (el plugin se declara en la raíz sin aplicarlo allí) y el build.gradle.kts del módulo, donde sí se aplica.
  • Que :app use otro módulo: build.gradle.kts (Module :app), con implementation(project(":torneo")).
  • Crear un módulo nuevo: File → New → New Module…; el asistente añade el include a settings.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.jvmargs de gradle.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.

ejemplo mínimo: implementation frente a api (con comentarios)
// 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.

ejemplo mínimo: namespace frente a applicationId (con comentarios)
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 marcador

pm 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.algo justo después de editar el catálogo: falta Sync Now, o el nombre no coincide (guion frente a punto, mayúsculas).
  • Unresolved reference a una librería que :torneo sí usa: está como implementation en :torneo y :app no la declara.
  • El módulo existe pero Gradle no lo encuentra: falta su include(":nombre") en settings.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:

ejemplo mínimo: dos strings.xml (con comentarios)
<!-- 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):

  1. Pide la lista de librerías de compilación de :torneo. Su configuración de compilación se llama compileClasspath.
  2. Pide la de :app en su versión debug. Su configuración se llama debugCompileClasspath.
  3. 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:

  1. ¿Qué línea tendrías que añadir, y dónde, para que :app pudiera usar Json? En build.gradle.kts (Module :app), implementation(libs.kotlinx.serialization.json), como hiciste con las corrutinas.
  2. ¿Y si una función pública de :torneo devolviera un tipo de esa librería? Entonces :torneo debería declararla con api, y :app la 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. 1.¿Qué fichero de Gradle dice qué módulos existen en el proyecto?

  2. 2.:torneo declara implementation(libs.kotlinx.coroutines.core) y :app depende de :torneo. ¿Qué ocurre si :app usa corrutinas sin declararlas y ninguna otra librería de :app las trae?

  3. 3.En :torneo (un módulo de Kotlin puro) declaras api(libs.kotlinx.serialization.json). ¿Qué cambia respecto a implementation?

  4. 4.En AndroidManifest.xml la pantalla se declara como android:name=".MainActivity", con un punto delante. ¿Con qué se completa ese nombre?

  5. 5.Un móvil está en español. Tu app tiene res/values/strings.xml y res/values-es/strings.xml. ¿Qué textos usa?

Faltan 5 de 5