Skip to main content

Dependencies, api versus implementation, and version catalogs

Plugin versus dependency​

A Gradle plugin adds or changes build behavior. A dependency supplies an artifact used by application code or by build-time tooling.

plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
}

dependencies {
implementation("com.squareup.okhttp3:okhttp:...")
}

The Android application plugin contributes Android variants and tasks for compiling, processing resources and manifests, producing DEX, packaging, signing, testing, and related work. The OkHttp dependency supplies classes that application code can call; it does not need to teach Gradle a new build process.

Plugins are not specifically "for generated code." Code-generation integration is only one kind of build behavior:

PluginBuild behavior it adds
Android applicationAndroid variants, packaging, signing, and APK/AAB production
Kotlin AndroidKotlin compilation for Android
KSPRuns symbol processors and connects generated output to compilation
HiltIntegrates Hilt-generated Android base classes through build configuration and bytecode transformation
Google ServicesProcesses google-services.json and generates Android resources
Kotlin serializationAdds serialization support to Kotlin compilation

How a dependency enters the Android build​

Most third-party libraries require only a dependency declaration because Gradle and Android Gradle Plugin already understand standard Maven metadata, JARs, and Android Archives (AARs).

group:name:version dependency coordinate
|
Gradle resolves repositories
|
downloads artifact + transitive dependencies
|
adds artifacts to the selected configuration
|
AGP compiles/merges/packages the relevant contents

A JAR mainly contains compiled JVM classes. An AAR can additionally contain Android resources, a manifest, assets, native libraries, and consumer R8 rules. AGP already knows how to merge and package these standard inputs, so a library-specific plugin is unnecessary unless the library needs custom build behavior.

The configuration describes where the artifact participates:

dependencies {
implementation(libs.okhttp) // compile and runtime use inside this module
compileOnly(libs.someAnnotations) // compile only
runtimeOnly(libs.someRuntime) // runtime only
testImplementation(libs.junit) // local tests
}

KSP: plugin versus processor dependency​

KSP means Kotlin Symbol Processing. Its Gradle plugin provides and schedules the symbol-processing tasks. A processor dependency contains library-specific logic that inspects symbols and decides which code to generate.

plugins {
alias(libs.plugins.ksp)
}

dependencies {
implementation(libs.hilt.android)
ksp(libs.hilt.compiler)
}

The responsibilities are deliberately separate:

KSP Gradle plugin
-> creates and schedules symbol-processing tasks
-> discovers and runs configured processors
-> connects generated output to Kotlin compilation

Processor dependency, such as hilt-compiler or room-compiler
-> recognizes its library's annotations and symbols
-> contains the rules for what code to generate
-> writes generated files through KSP

Therefore, the processor dependency normally generates the library-specific code; KSP operates the generation machinery and includes its output in the build. Generated output normally appears under variant-specific directories such as:

app/build/generated/ksp/debug/
app/build/generated/ksp/release/

Treat everything under build/ as generated output. Do not edit it manually.

Why Hilt uses dependencies and plugins​

Hilt illustrates why these roles cannot be reduced to "plugin versus API":

Hilt Gradle plugin -> Android build integration and bytecode transformation
KSP Gradle plugin -> symbol-processing task integration
hilt-android -> annotations and runtime APIs used by application code
hilt-compiler -> Hilt-specific processor and code-generation rules

The compiler generates Hilt components and Android base classes. The KSP plugin runs that compiler and connects its output to compilation. The Hilt Gradle plugin integrates the generated base classes so source code can extend normal Android classes:

@HiltAndroidApp
class ArchitectApplication : Application()

Without the Hilt Gradle plugin's transformation, code would need to reference a generated Hilt base class directly. Dependencies alone can supply APIs and processors, but they do not automatically add every custom transformation or task configuration a library's Android integration requires.

Mental model

Use a dependency when existing Gradle/AGP behavior already knows how to consume the artifact. Apply a plugin when the build needs additional behavior. For generated code, ask two separate questions: which plugin schedules and connects generation, and which processor dependency knows what to generate?

Dependency configuration is an architectural boundary​

In a library module:

dependencies {
implementation(libs.okhttp)
api(projects.models.publicApi)
}
  • implementation: needed internally; not intentionally exposed on consumer compile API.
  • api: types are part of the module’s public contract and must be visible to consumers.

Concrete example​

// :network public API
interface CatalogClient {
suspend fun products(): List<Product>
}

If public method signatures expose no OkHttp type, use implementation(libs.okhttp). Consumers should not compile against OkHttp merely because this module happens to implement HTTP with it.

❌ Leaky public API​

fun client(): OkHttpClient

Now OkHttp is part of the module contract, so either api is required or the API should be redesigned around owned abstractions.

Why overusing api hurts​

  • larger compile classpaths;
  • more downstream recompilation when exposed ABI changes;
  • tighter module coupling;
  • accidental reliance on transitive dependencies;
  • harder replacement/migration.

Do not mechanically replace everything with implementation; public signatures determine correct exposure.

Other configurations​

ConfigurationAvailability
compileOnlyCompile but not runtime/package; consumer/runtime must provide
runtimeOnlyRuntime but not compile API
testImplementationLocal unit tests
androidTestImplementationInstrumented tests
kspKSP processors
lintChecksCustom lint rule artifact

Misusing compileOnly can produce release ClassNotFoundException if runtime does not actually supply the library.

Version catalog​

[versions]
kotlin = "..."
room = "..."

[libraries]
room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" }

[plugins]
android-application = { id = "com.android.application", version = "..." }

Catalogs centralize aliases/versions, but they do not guarantee compatibility or force all modules to use one version by themselves. Dependency resolution, platforms/BOMs, constraints, and lock/verification policy still matter.

Dynamic versions are unstable inputs​

❌ Avoid​

implementation("com.example:sdk:1.+")

The same commit can resolve differently tomorrow, breaking reproducibility/cache correctness.

✅ Prefer pinned reviewed versions​

Use dependency update automation with changelog/security review and controlled merge.

Dependency conflict​

Two libraries request different versions. Gradle resolution selects according to metadata/rules, but “highest wins” does not prove binary/runtime compatibility.

Investigate with dependency insight, inspect why each version is present, align using BOM/platform/constraints where valid, and test runtime paths.

Dependency verification and repositories​

Restrict repositories, verify checksums/signatures according to threat model, avoid arbitrary project-added repositories, and keep credentials in protected CI/user configuration—not build scripts.

Dependency evaluation checklist​

  • product need and alternatives;
  • maintenance/release/security health;
  • size/method/native ABI contribution;
  • startup initialization/providers;
  • permissions/manifest additions;
  • data/privacy behaviour;
  • transitive graph and license;
  • R8 rules and compatibility;
  • exit/migration plan.
Interview-ready answer

implementation keeps an internal dependency off consumers’ intended compile API, while api is correct when dependency types are exposed in the library’s public ABI. Overusing api increases coupling and recompilation. I pin versions, use catalogs/BOMs/constraints intentionally, inspect dependency graphs and release manifests, verify supply-chain inputs, and evaluate size, startup, privacy, security, and removal cost before adding an SDK.

Exercises​

  1. Decide api/implementation for three public signatures.
  2. Diagnose NoClassDefFoundError caused by compileOnly.
  3. Write a third-party analytics SDK evaluation checklist.