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.
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
– LikecompileOnly
, 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.
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.
// 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 accesslifecycle-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.
// 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.
// 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.
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.
// 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.
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.
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.
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.
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.
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!