Por qué esta decisión importa
En un proyecto chico, muchas veces parece que da lo mismo usar implementation, api o cualquier otra configuration. La app compila igual, los tests pasan y Android Studio no se queja. Entonces la decisión se ve como una preferencia de estilo. No lo es.
En un proyecto con varios módulos, esas tres palabras cambian qué clases quedan visibles desde otros módulos, cuánto acoplamiento creás y hasta cuánto recompila Gradle cuando tocás algo aparentemente aislado. Por eso no es solo una cuestión de build: también es una cuestión de diseño.
Qué hace implementation
implementation agrega una dependencia que el módulo actual puede usar, pero intenta mantenerla encapsulada hacia afuera. Es la opción por defecto en la mayoría de los casos.
dependencies {
implementation(libs.coroutines.core)
implementation(project(":core:network"))
}
La idea es simple: este módulo necesita esa dependencia para funcionar, pero los módulos consumidores no deberían depender de sus detalles internos. Eso ayuda a mantener una frontera más limpia.
Punto claveimplementation expresa intención de encapsulamiento. No dice “esta librería existe en el proyecto”, dice “este módulo la usa internamente”.
Qué hace api
api expone la dependencia como parte de la superficie visible del módulo. Si otro módulo depende de este, también va a ver la dependencia transitivamente en su classpath de compilación.
dependencies {
api(project(":core:model"))
}
Eso tiene sentido cuando el módulo realmente forma una API compartida. Por ejemplo, si un módulo exporta tipos de dominio, contratos o clases que otros módulos necesitan referenciar directamente. En ese caso esconder la dependencia sería artificial, porque esos tipos forman parte del contrato público.
El problema es que api se vuelve tentadora. “Lo pongo así y listo” parece resolver errores de compilación rápido. Pero también agranda el classpath visible y hace que más módulos queden atados entre sí.
Qué hace compileOnly
compileOnly hace que una dependencia esté disponible al compilar, pero no en runtime ni en el artefacto final. Es útil cuando el entorno que ejecuta el código ya provee esa dependencia o cuando la necesitás solo como contrato de compilación.
dependencies {
compileOnly("javax.annotation:javax.annotation-api:1.3.2")
}
En Android puro se usa menos que implementation, pero sigue siendo válida en ciertos escenarios: annotations, APIs provistas externamente, integraciones de plugins o casos muy controlados de librerías.
AtenciónSi usás compileOnly para algo que en runtime tu app realmente necesita, la compilación puede pasar y la falla aparecer recién al ejecutar.
Casos reales
Módulo de feature que usa Retrofit internamente
Si :feature:login usa Retrofit para hablar con una API, casi siempre corresponde implementation. El resto del proyecto no necesita saber que esa feature usa Retrofit.
dependencies {
implementation(libs.retrofit)
implementation(libs.okhttp)
}
Módulo de contratos o modelos compartidos
Si tenés un módulo :core:model y querés que otros módulos vean esos tipos porque forman parte del contrato público, ahí api puede ser correcto.
Annotations o APIs solo de compilación
Si necesitás ciertas anotaciones o contratos para compilar, pero no forman parte del runtime final, compileOnly puede evitar meter peso innecesario o falsas transitividades.
Regla práctica para decidir
La regla simple que suele funcionar mejor es esta:
- Usá
implementationpor defecto. - Subí a
apisolo cuando la dependencia forme parte de la API pública del módulo. - Usá
compileOnlycuando necesites compilar contra algo que no debés empaquetar ni esperar que Gradle lleve al runtime.
Regla mentalSi al borrar la dependencia transitiva los módulos consumidores no deberían enterarse, era implementation. Si sí deberían enterarse, quizá era api.
Errores comunes
- Usar
apipara “hacer que compile” sin revisar si el módulo realmente debería exponer esa dependencia. - Meter casi todas las librerías en el módulo
appy perder fronteras claras entre features. - Usar
compileOnlysin confirmar quién provee esa dependencia en runtime. - Asumir que si la app compila, la elección fue buena.
La calidad de esta decisión se nota más con el tiempo que en el primer commit. Cuando el proyecto crece, usar mal estas configurations se traduce en más acoplamiento, builds menos predecibles y una arquitectura que se “filtra” entre módulos.
Cierre
implementation, api y compileOnly no son sinónimos con distintos nombres. Son herramientas para expresar fronteras técnicas concretas. En proyectos Android serios, elegir bien entre ellas ayuda a que Gradle compile mejor, pero sobre todo a que el proyecto esté mejor pensado.