Stuart Kent
on Android

Listing Your Android App's (Actual) Dependencies In 2020

Gradle lets us list all the dependencies of our projects using the dependencies task. Here’s a small snippet of sample output:

+--- org.jetbrains.kotlin:kotlin-android-extensions-runtime:1.3.61
|    \--- org.jetbrains.kotlin:kotlin-stdlib:1.3.61
|         +--- org.jetbrains.kotlin:kotlin-stdlib-common:1.3.61
|         \--- org.jetbrains:annotations:13.0
+--- org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.3.61
|    \--- org.jetbrains.kotlin:kotlin-stdlib:1.3.61 (*)
+--- androidx.appcompat:appcompat:1.1.0
|    +--- androidx.annotation:annotation:1.1.0
|    +--- androidx.core:core:1.1.0
|    |    +--- androidx.annotation:annotation:1.1.0
|    |    +--- androidx.lifecycle:lifecycle-runtime:2.0.0 -> 2.1.0
.    .
.    .
.    .

This output shows both the direct and indirect (aka transitive) dependencies of our project. Indirect dependencies are indicated by indentation, so in the example above:

Running ./gradlew :app:dependencies on an Android project outputs many separate lists of dependencies; one for each Gradle configuration. While the term may sound unfamiliar, you’ve definitely interacted with configurations before; the implementation keyword you use when declaring dependencies of your app refers to a configuration with the same name.

We can reduce the verbosity of the dependencies task by passing the name of a single configuration using the --configuration parameter:

./gradlew :app:dependencies --configuration someConfiguration

The rest of this post explains which Gradle configurations we should use with the dependencies task as Android developers in 2020. Hint: not implementation :)

Historical Context

Prior to Gradle 3.4, configurations could be used for one or more of several distinct purposes:

  1. holding a list of direct dependencies declared via the dependencies block of our build.gradle files;
  2. resolving and operating on a complete dependency tree (effectively a classpath) based on a list of direct dependencies and a target usage (compilation, runtime, etc);
  3. declaring project artifacts for other projects to consume.

In these versions of Gradle, the typical advice for Android developers was to run the dependencies task using the compile configuration:

./gradlew :app:dependencies --configuration compile

This did exactly what you probably guessed; it output the full dependency tree based on the dependencies you’d declared to be part of the compile configuration in your build.gradle file’s dependencies block. However, the target usage being considered was not immediately apparent.

Modern Gradle

In Gradle 3.4, new configurations were restricted to each being used for exactly one of the three purposes described above. This improved separation of concerns allowed some previously-impossible performance optimizations to be implemented.

As part of this change, the compile configuration was deprecated and replaced by the api and implementation configurations. Running ./gradlew :app:dependencies --configuration compile now results in the output below:

compile - Compile dependencies for 'main' sources (deprecated: use 'implementation' instead).
No dependencies

You might guess that the proper way to inspect Android app dependencies using modern Gradle versions is to run

./gradlew :app:dependencies --configuration implementation

If you try this, you’ll see output similar to:

implementation - Implementation only dependencies for 'main' sources. (n)
+--- org.jetbrains.kotlin:kotlin-android-extensions-runtime:1.3.61 (n)
+--- org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.3.61 (n)
+--- androidx.appcompat:appcompat:1.1.0 (n)
+--- androidx.core:core-ktx:1.1.0 (n)
+--- com.google.android.material:material:1.0.0 (n)
+--- maps-sdk-3.0.0-beta (n)
+--- com.google.android.gms:play-services-location:17.0.0 (n)
\--- com.google.android.gms:play-services-gcm:17.0.0 (n)

(n) - Not resolved (configuration is not meant to be resolved)

This output only includes our direct dependencies and does not include any indirect dependencies, reducing its usefulness. The clue as to why is in the note at the bottom:

(configuration is not meant to be resolved)

This is signalling to us that the implementation configuration is only for holding a list of direct dependencies and (unlike the deprecated compile configuration) cannot also be used for resolving a complete dependency tree.

The upgrade notes for Gradle 6 indicate that compileClasspath and runtimeClasspath are the configurations we should use for dependency resolution in modern Gradle projects:

The implementation, api, compileOnly and runtimeOnly configurations should be used to declare dependencies and the compileClasspath and runtimeClasspath configurations to resolve dependencies.

The relationships between these configurations are shown in the diagram below1. Configurations that can be used to resolve a complete dependency tree are marked (R) and colored blue.

In Android projects, there is one compile and one runtime classpath configuration for each build variant. These configurations are named {variant}CompileClasspath and {variant}RuntimeClasspath. In a project with default build variants, we can therefore inspect our resolved dependency tree using one of the following commands:

./gradlew :app:dependencies --configuration debugCompileClasspath
./gradlew :app:dependencies --configuration releaseCompileClasspath
./gradlew :app:dependencies --configuration debugRuntimeClasspath
./gradlew :app:dependencies --configuration releaseRuntimeClasspath

If you use custom build variants, replace debug or release with the name of the variant you are interested in inspecting. If you declare compileOnly or runtimeOnly dependencies, pay more attention to whether you wish to inspect resolved compilation ({variant}CompileClasspath) or runtime ({variant}RuntimeClasspath) dependencies and select your configuration accordingly.

Dependency Insights

{variant}CompileClasspath and {variant}RuntimeClasspath are also appropriate configurations to pass as parameters to the dependencyInsight Gradle task. This task provides more details on a specific dependency and how it was resolved, and requires us to pass a configuration. An example invocation and output snippet is shown below:

./gradlew app:dependencyInsight \
        --dependency androidx.appcompat:appcompat: \
        --configuration releaseCompileClasspath
androidx.appcompat:appcompat:1.1.0
   ...
   Selection reasons:
      - By constraint : releaseRuntimeClasspath uses version 1.1.0