Debugging Dependency Resolution Errors in Android Development

Kayvan Kaseb
Software Development
8 min readApr 8, 2024
The picture is provided by Unsplash

As a matter of fact, effective management of dependencies is vital for smooth Android project builds. Developers need to troubleshoot conflicts and compatibility issues efficiently. The means the proactive dependency management can improve development workflows and app quality. However, debugging dependency resolution errors could be considered as a tricky task in Android. This article aims to discuss some steps and best-practices to address dependency resolution errors in practice.

Introduction

Gradle Build Tool (Gradle) is indeed an open-source build automation tool known for its speed, reliability, and flexibility. It uses a declarative build language to define project configurations and dependencies, making it easy to manage complex build processes. So, Gradle is the most popular build system for the JVM, and is the default system for Android and Kotlin projects. This means it has a rich community plugin ecosystem.

Moreover, Dependency Resolution Errors can occur when building or syncing an Android project in Android Studio. In general, these errors come from conflicts or inconsistencies in the dependencies declared in your project’s build.gradle files. Debugging dependency resolution errors involves identifying and resolving these issues to ensure that your project can build successfully and function as intended.

In Android development, dependencies are external libraries or modules, which your project relies on to compile and run. Dependency resolution is the process of specifying which versions of these dependencies to use and ensuring that all required dependencies are available.

Fixing Dependency Resolution Errors

Debugging dependency resolution errors in Android development can be considered as a tricky issue, but there are several steps you can take to identify and fix these issues as follows:

Checking Gradle Console Output: Start by examining the Gradle console output. Look for any error messages or warnings related to dependency resolution. These messages often provide clues about what went wrong.

Reviewing Build.gradle Files: Verify that your project’s build.gradle files (both project-level and module-level) are configured correctly. Check for any typos, missing or incorrect dependencies, or version conflicts. For example, common mistakes include misspelled keywords, missing curly braces, or incorrect indentation. Besides, ensure that the dependencies block in your module-level build.gradle file consists of all the necessary dependencies for your project. Each dependency should be determined with its group ID, artifact ID, and version.

Updating Dependencies: Ensure that you are using the latest versions of your dependencies. Sometimes, conflicts arise because of outdated versions. Use the latest stable versions whenever possible. You can visit the official websites or repositories of your dependencies to check for the latest stable versions. Also, you can use online resources, like Maven Central or JCenter to search for the latest versions of your dependencies.

Checking Repository URLs: Confirm that the repository URLs specified in your build.gradle files are correct. If you are using custom repositories, make sure they are accessible and configured properly.

pluginManagement {
repositories {
gradlePluginPortal()
google()
mavenCentral()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}
rootProject.name = "My Application"
include ':app'

The above code is using Gradle’s new dependency resolution management feature introduced in Gradle 7. This feature allows you to define repository settings at the settings.gradle file level, which makes effect the entire project’s dependency resolution behavior. This block is where you configure repositories and dependencies used by all modules in your project. However, it is important to note that module-specific dependencies should be configured in each module-level build.gradle file.

Check for Compatibility Issues: Sometimes, dependencies may not be compatible with each other due to conflicting APIs or functionality. Make sure that the dependencies you are using are compatible with each other and with your project’s target SDK version. Therefore, you should read the documentation of each dependency you are using. If you encounter runtime errors or crashes, review the error logs for any compatibility-related issues.

Execution failed for task ':app:compileDebugJavaWithJavac'.
> Could not resolve all files for configuration ':app:debugCompileClasspath'.
> Could not find com.google.guava:guava:21.0.

Use Gradle Dependency Insight: Gradle provides a helpful tool called Dependency Insight, which allows you to inspect the dependency tree and understand why a particular version of a dependency was chosen. In other words, Gradle offers the built-in dependencyInsight task to render a dependency insight report from the command line. Dependency insights provide information about a single dependency within a single configuration.

> gradle -q dependencyInsight --dependency commons-codec --configuration scm
commons-codec:commons-codec:1.7
Variant default:
| Attribute Name | Provided | Requested |
|-------------------|----------|-----------|
| org.gradle.status | release | |
Selection reasons:
- By conflict resolution: between versions 1.7 and 1.6

commons-codec:commons-codec:1.7
\--- scm

commons-codec:commons-codec:1.6 -> 1.7
\--- org.apache.httpcomponents:httpclient:4.3.6
\--- org.eclipse.jgit:org.eclipse.jgit:4.9.2.201712150930-r
\--- scm

A web-based, searchable dependency report is available by adding the --scan option.

Resolve Version Conflicts: If you tackle version conflicts, you will need to resolve them manually. You can be able to force a specific version of a dependency by adding it to your build.gradle file with the force attribute.

plugins {
id 'java' // so that there are some configurations
}

configurations.all {
resolutionStrategy.force 'asm:asm-all:3.3.1', 'commons-io:commons-io:1.4'
}

Exclude Transitive Dependencies: If a dependency brings in its own dependencies that conflict with your project’s dependencies, you can be able to exclude those transitive dependencies. For example:

dependencies {
implementation('com.example:library:1.0.0') {
exclude group: 'com.example', module: 'conflicting-library'
}
}

In addition, suppose we are using the Gson library (com.google.code.gson:gson) in our project, but we face a conflict because another library, we can call it "conflicting-library", includes its own version of Gson, that causes a version conflict.

dependencies {
implementation 'com.google.code.gson:gson:2.8.7'
implementation 'com.example.conflicting-library:library:1.0.0'
}

In this case, “conflicting-library” contains its own version of Gson, which conflicts with the version we’ve explicitly declared. When you try to sync your project or build it, Gradle may throw an error. To solve this error using the exclude directive, you can modify your dependencies block as follows:

dependencies {
implementation('com.google.code.gson:gson:2.8.7') {
exclude group: 'com.google.code.gson', module: 'gson'
}
implementation 'com.example.conflicting-library:library:1.0.0'
}

By adding the exclude directive, you instruct Gradle to exclude the Gson module from being pulled in transitively from "conflicting-library". This means you ensure that only the version of Gson explicitly declared in your dependencies block is used in your project. So, with this modification, the error should be resolved, and your project should sync and build successfully without any conflicts related to Gson versions.

Fix Conflicts between Classpaths

Basically, when Gradle resolves the compile classpath, it first resolves the runtime classpath and uses the result to specify what versions of dependencies should be added to the compile classpath. This means that the versions selected for dependencies on the runtime classpath influence the versions that will be applied for identical dependencies on the compile classpath. Conflicts may arise when your application and a library module include different versions of the same dependency. This can happen, for instance, if your app includes a version using the implementation dependency configuration, while the library module includes a different version using the runtimeOnly configuration.

When managing dependencies within your runtime and compile-time classpaths, Android Gradle plugin versions 3.3.0 and above aim to resolve specific version conflicts automatically. For example, if there is a difference where the runtime classpath includes Library A version 3.0 and the compile classpath includes Library A version 2.0, the plugin will automatically adjust the dependency on the compile classpath to match Library A version 3.0. Thus, it diminishes any potential errors. Nevertheless, if Library A version 2.0 is in the runtime classpath and Library A version 3.0 is in the compile classpath, the plugin will not switch the compile classpath to Library A version 2.0. As a result, you might encounter an error as follows:

Conflict with dependency 'com.example.library:some-lib:2.0' in project 'my-library'.
Resolved versions for runtime classpath (1.0) and compile classpath (2.0) differ.

To solve this error, you can either align the versions of the dependency or exclude the conflicting version from one of the configurations. For instance, you can exclude the conflicting version:

dependencies {
implementation('com.example.library:some-lib:2.0') {
exclude group: 'com.example.library', module: 'some-lib'
}
runtimeOnly 'com.example.library:some-lib:1.0'
}

Furthermore, you can include the wanted version of the dependency as an api dependency to your library module. In other words, only your library module declares the dependency, but the app module will also have access to its API, transitively. For example:

dependencies {
api 'com.example.library:some-lib:2.0'
api 'com.example.library:some-lib:1.0'
}

However, it is important to note that the api configuration is typically used for dependencies that are part of your project's public API. If com.example.library:some-lib is not part of your project's public API but is used internally, it might be more appropriate to use implementation for both dependencies. It is significant to carefully consider which dependencies should be part of your project’s API and which should be internal dependencies used only within your project.

The Project’s Public API and Internal Dependencies

Understanding the distinction between a project’s public API and internal dependencies is important for handling dependencies effectively in a software project. The public API of a project consists of the classes, methods, and interfaces that are intended to be used by external consumers of the project. These external consumers can be other modules within the same project, other projects within the same codebase, or external applications or libraries. Suppose we are developing a library called “MathUtils” that offers different mathematical utilities for external users. We want to expose some utility functions as part of the library’s public API:


public class MathUtils {
public static int add(int a, int b) {
return a + b;
}

public static int subtract(int a, int b) {
return a - b;
}
}

So, Gradle dependency declaration should be:

dependencies {
api 'com.example.mathutils:math-utils:1.0'
}

In contrast, internal dependencies are dependencies that are applied within the project, but are not intended to be part of the project’s public API. They are internal implementation details and are not meant to be accessed or used directly by external consumers of the project. For instance, our MathUtils library internally uses a logging library called “Logger” to log debug messages during computation. However, we do not want external users to have direct access to the logging functionality; it’s an internal detail of our library.


import com.example.logger.Logger;

public class MathUtils {
public static int add(int a, int b) {
Logger.debug("Adding " + a + " and " + b);
return a + b;
}

public static int subtract(int a, int b) {
Logger.debug("Subtracting " + b + " from " + a);
return a - b;
}
}

As a result, Gradle dependency declaration is:

dependencies {
implementation 'com.example.logger:logger-lib:1.0'
}

Leverage Dependency Management Plugins

Consider using dependency management plugins. These plugins automate dependency resolution and often support conflict resolution mechanisms. For example, when using AndroidX Jetpack libraries, it is recommended to leverage the Bill of Materials (BOM) provided by Google. Compose Bill of Materials (BOM) allows you handle all of your Compose library versions by determining just only the BOM’s version. The BOM helps manage dependency versions and resolves conflicts automatically.

dependencies {
// Import the Compose BOM
implementation platform('androidx.compose:compose-bom:2024.04.00')

// Import Material Design 3 library
implementation 'androidx.compose.material3:material3:1.1.2'

// Libraries without version numbers
implementation 'androidx.compose.foundation:foundation'
}

In Conclusion

Handling dependencies in Android projects is essential for a smooth build process. Understanding conflicts and compatibility issues helps developers troubleshoot effectively. Tasks like aligning versions, using Gradle features, and thorough testing are significant. Automatic resolution features, like those in the Android Gradle plugin, also aid in managing dependencies. Overall, proactive dependency management enhances development workflows and app quality. This essay explored some steps and best-practices to address dependency resolution errors in practice.

--

--

Kayvan Kaseb
Software Development

Senior Android Developer, Technical Writer, Researcher, Artist, Founder of PURE SOFTWARE YAZILIM LİMİTED ŞİRKETİ https://www.linkedin.com/in/kayvan-kaseb