Android Gradle Dependency Configurations: 8 Key Types, Differences & Best Practices

Table of Contents

Gradle is an essential build tool for Android development, managing dependencies and automating the build process. One of the critical aspects of Gradle is its dependency configurations, which control how dependencies are included in a project. In this article, we will explore different Gradle dependency configurations in Android, explaining their usage with Kotlin DSL (build.gradle.kts) examples.

What Are Gradle Dependency Configurations?

In Gradle, dependencies have specific scopes, meaning they are used in different phases of your project.

  • Some dependencies are needed during compilation to build your source code.
  • Others are required only at runtime when the application is running.
Kotlin
dependencies {
    implementation("androidx.core:core-ktx:1.12.0")   // Needed for compiling and running the app
    runtimeOnly("com.squareup.okio:okio:3.3.0")      // Needed only at runtime (e.g. for file I/O)
}

Gradle manages these scopes using Configurations, which define when and how dependencies are used in the build process. Dependency configurations help organize dependencies based on their purpose, such as:

  • Compiling source code
  • Running the application
  • Testing the project

These configurations ensure that dependencies are available at the right stage of development, helping Gradle resolve them efficiently.

Commonly Used Gradle Dependency Configurations in Android

For Main Code (Application/Library Code)

  • api – Used for both compilation and runtime. Also included in the published API (for libraries).
  • implementation – Used for compilation and runtime, but not exposed in the published API.
  • compileOnly – Used only for compilation, not available at runtime.
  • compileOnlyApi – Like compileOnly, but included in the published API.
  • runtimeOnly – Used only at runtime, not needed for compilation.

For Tests

  • testImplementation – Used for compiling and running tests.
  • testCompileOnly – Used only for compiling tests, not available at runtime.
  • testRuntimeOnly – Used only at test runtime, not needed for compilation.

Have you noticed this — what does ‘published API’ mean?

When we say ‘published API,’ we are talking about the public API of our module or library — the part that other modules or projects can see and use.

For example, if we are creating a library (myLibrary) that will be used by another project (otherApp), some dependencies need to be exposed to the users of our library, while others should remain internal.

Now, let’s break down each configuration with explanations and see how we will use it in our code.

implementation

The implementation configuration is the most commonly used dependency type. It ensures that a module can access the dependency but does not expose it to other modules.

Kotlin
dependencies {
    implementation("androidx.core:core-ktx:1.9.0")
    implementation("com.squareup.retrofit2:retrofit:2.9.0")
}

Here,

  • The implementation keyword ensures that the dependency is only available to the module where it is declared.
  • Other modules cannot access these dependencies, reducing compilation time and improving modularity.
  • Also, dependencies declared with implementation are available at compile time and runtime.

api

The api configuration behaves similarly to implementation, but it exposes the dependency to other modules that depend on that particular module. This means we should use api when we want to expose a dependency to other modules that depend on our module.

Kotlin
// Adding a dependency that should be exposed to other modules
dependencies {
    api("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1")
}
  • If Module A includes this dependency with api, then Module B (which depends on Module A) can also access lifecycle-viewmodel-ktx.
  • Use api for both compilation and runtime, but only when the dependency is part of your public API.
  • This is useful for creating libraries or shared modules.

compileOnly

The compileOnly configuration includes dependencies only during compilation but excludes them at runtime.

Kotlin
// Adding a dependency that is required only at compile time
// Lombok is a Java library, but we can also use it in Android to reduce boilerplate code by providing annotations
dependencies {
    compileOnly("org.projectlombok:lombok:1.18.36")
    annotationProcessor("org.projectlombok:lombok:1.18.36")
    kapt("org.projectlombok:lombok:1.18.36") // kotlin project(Kotlin KAPT plugin)
}
  • compileOnly is typically used for annotation processors or compile-time dependencies that are not needed when the application is running.
  • Useful for lightweight dependencies that assist in code generation.
  • The dependency will not be included in the final APK.

compileOnlyApi

The compileOnlyApi configuration behaves like compileOnly, but it also exposes the dependency to modules that depend on this module.

Kotlin
// Adding a compile-only dependency that should be exposed
dependencies {
    compileOnlyApi("org.jetbrains:annotations:20.1.0")
}
  • The module itself uses this dependency only during compilation.
  • Other modules that depend on this module can also access the dependency.
  • Useful when developing shared libraries where compile-time dependencies need to be propagated.

runtimeOnly

The runtimeOnly configuration ensures that the dependency is available only at runtime, not during compilation.

Kotlin
dependencies {
    implementation("com.jakewharton.timber:timber:5.0.1") // Available at compile-time
    runtimeOnly("com.github.tony19:logback-android:3.0.0") // Only used at runtime for logging, logback-android is a backend logging framework that processes logs at runtime.
}
  • The code is compiled without logback-android because it is not available during compilation.
  • runtimeOnly ensures that the dependency is available only at runtime and is not included in the compilation classpath.
  • Helps in keeping the compilation classpath clean.

testImplementation

testImplementation is used when you need a dependency for both compiling and running test cases. This is the most commonly used configuration for test dependencies.

Kotlin
// Adding a testing library
dependencies {
    testImplementation("junit:junit:4.13.2")
}
  • testImplementation ensures that the JUnit library is only available in the test environment.
  • It does not get bundled into the final application.

testCompileOnly

testCompileOnly is used when a dependency is required only at test compile-time but is not needed at runtime. This is useful when a dependency provides compile-time annotations or APIs but doesn’t need to be included during test execution.

Kotlin
dependencies {
    testCompileOnly 'junit:junit:4.13.2'  // Available at compile-time but not included at runtime
}

This means,

  • The dependency is only available during compilation for unit tests.
  • It is not included in the test runtime classpath.
  • Use it when you need a library at compile time (e.g., compile-time annotations) but don’t want it in the runtime environment.

testRuntimeOnly

testRuntimeOnly is used when a dependency is needed only at test runtime and is not required at compile-time.

Kotlin
dependencies {
    testRuntimeOnly("org.junit.vintage:junit-vintage-engine:5.9.1")
}

Here,

  • The junit-vintage-engine is used only for executing JUnit 4 tests under the JUnit 5 framework.
  • Means, junit-vintage-engine is used to bridge the gap when you are using the latest version, JUnit 5, but still need to test some functionality that primarily relies on JUnit 4 or JUnit 3. In such cases, it allows you to run these older JUnit 3 and JUnit 4 tests alongside your new JUnit 5 tests within the same JUnit 5 test runner.
  • It is not needed for compilation but must be available when running tests.

Bonus

androidTestImplementation

Similar to testImplementation, but specifically for Android instrumentation tests that run on a device or emulator.

Kotlin
dependencies {
    androidTestImplementation("androidx.test.ext:junit:1.1.3")
}
  • The JUnit extension for Android testing is only available for instrumented tests (which run on an actual device or emulator).
  • Helps in isolating dependencies specific to Android UI tests.

annotationProcessor

Used for annotation processors that generate code at compile time.

Kotlin
dependencies {
    annotationProcessor("androidx.room:room-compiler:2.4.2")
}

// Note: Even though I defined it in a Kotlin file, it is actually in the Gradle file (build.gradle).  
// Also, annotationProcessor is mostly used for Java-based projects; its alternative in Kotlin is KAPT, 
// which we will see just after this.
  • Room’s compiler processes annotations at compile time and generates necessary database-related code.
  • Typically used with libraries like Dagger, Room, and ButterKnife.

kapt (Kotlin Annotation Processing Tool)

Since annotationProcessor does not work with Kotlin, we use kapt instead.

Kotlin
dependencies {
    kapt("androidx.room:room-compiler:2.4.2")
}
  • kapt handles annotation processing in Kotlin.
  • Required for libraries that rely on annotation processing.

Conclusion

Understanding Gradle dependency configurations is crucial for managing dependencies efficiently in Android development. By using the right configurations, you can improve build performance, maintain a modular project structure, and avoid unnecessary dependencies. By following best practices, you can make your Gradle build system cleaner and more maintainable.

With these insights and examples, you should now have a clear understanding of Gradle dependency configurations in Android and how to use them effectively!

Skill Up: Software & AI Updates!

Receive our latest insights and updates directly to your inbox

Related Posts

error: Content is protected !!