SDK Dependencies Resolution on Android

The dependency issue arises around shared packages or libraries on which several other packages have dependencies, but where they depend on different and incompatible versions of the shared packages. Gradle has made the lives of Android developers quite easy - just add one dependency in the build.gradle, and the required library is seamlessly included in the build. But what happens when two dependencies have a dependency on different versions of the same library?

Gradle is able to automatically resolve dependencies if both dependencies belong to same configuration, i.e. app configuration. For example, Audience Network Android SDK depends on "exoplayer" library. If you need to add a different version of "exoplayer" library, Gradle will choose the higher version to include. But if both the dependencies belong to different configurations, i.e app and test, then Gradle will throw an error. Below we will go through the different conflicts and solutions.

Prerequisites

Ensure you have completed the Audience Network Getting Started and Android Getting Started guides before you proceed.

Dependency Conflicts

1: Shared Libraries in Dependencies from Same Configuration

2: Shared Libraries in Dependencies from Different Configurations

Dependency Conflicts Solutions

1: Let Gradle Resolve Dependencies for you

2: Exclude Specific Version of Dependency

3: Explicitly Define the Conflicted Library in Gradle

4: Force Resolution of the Library



Dependency Conflicts

1: Shared Libraries in Dependencies from Same Configuration

Consider the following example. Both dependencies in the below code stay in same configuration and have an internal dependency on a library "org.hamcrest:hamcrest-core". Because both dependencies are internally using different versions of the same library, the highest version gets included in the build. The output of the Gradle sync will clearly indicate that it automatically upgrades the hamcrest library version from 1.1 to 1.3 in the final build.

// Depends on version 1.3 of org.hamcrest:hamcrest-core
androidTestImplementation 'junit:junit:4.12' 
// Depends on version 1.1 of org.hamcrest:hamcrest-core
androidTestImplementation 'org.mockito:mockito-core:1.10.19'  

2: Shared Libraries in Dependencies from Different Configurations

If both dependencies belong to different configurations i.e app and test configuration, then Gradle will throw an error. Consider the following code snippet. The first dependency belongs to the app configuration, while the second dependency belongs to the test configuration. So, while building the project, it will fail with exception.

When instrumentation tests are run, both the main APK and test APK share the same classpath. Gradle build will fail if the main APK and the test APK use the same library (e.g. Guava) but in different versions. If Gradle didn’t catch that, your app could behave differently during tests and during normal run (including crashing in one of the cases).

// Depends on version 1.3 of org.hamcrest:hamcrest-core 
implementation 'junit:junit:4.12' 
// Depends on version 1.1 of org.hamcrest:hamcrest-core
androidTestImplementation 'org.mockito:mockito-core:1.10.19' 

Dependency Conflicts Solutions

1: Let Gradle Resolve for You

This approach is the simplest one but limited to dependencies in same configurations. For example, your module explicitly depends on a specific version of ExoPlayer. However, the Audience Network SDK already has included another version of ExoPlayer. On default, the highest version gets included in the build. With code snippet below, the output of Gradle sync will clearly indicate that it automatically upgrades the ExoPlayer library version from r2.4.2 to 2.7.3 in the final build.

implementation 'com.google.android.exoplayer:exoplayer-core:2.7.3'
implementation 'com.google.android.exoplayer:exoplayer-dash:2.7.3'
...
// audience-network-sdk depends on exoplayer-core:r2.4.2 and exoplayer-dash:r2.4.2
implementation 'com.facebook.android:audience-network-sdk:4.28.1' 

2: Exclude Specific Version of Dependency

In most situations, developers may want to have control over which version of the library will be finally included in the build. You are allowed to configure build.gradle to make it happen. For example, if your developer prefers the lower version of ExoPlayer r2.4.0 instead of r2.4.2 in Audience Network SDK, then you can exclude that module while declaring "audience-network-sdk" dependency. The exclude tag applies to dependencies in different configurations, too.

In a real project, there will be many dependencies which will have different versions of the same library. In that case, for each and every dependency, you will need to have the exclude tag such that you can include expected version of that library.

Scenario 1: Dependencies in Same Configuration
implementation 'com.google.android.exoplayer:exoplayer-core:r2.4.0'
implementation 'com.google.android.exoplayer:exoplayer-dash:r2.4.0'
...
// audience-network-sdk depends on exoplayer-core:r2.4.2 and exoplayer-dash:r2.4.2
implementation ('com.facebook.android:audience-network-sdk:4.28.1') {
    exclude group: 'com.google.android.exoplayer', module:'exoplayer-core'
    exclude group: 'com.google.android.exoplayer', module:'exoplayer-dash'
}
Scenario 2: Dependencies in Different Configuration
// Depends on version 1.3 of org.hamcrest:hamcrest-core 
implementation 'junit:junit:4.12' 
// Depends on version 1.1 of org.hamcrest:hamcrest-core
androidTestImplementation ('org.mockito:mockito-core:1.10.19'){
    exclude group: 'org.hamcrest', module:'hamcrest-core'
}

3: Explicitly Define the Conflicted Library in Gradle

This is a cleaner way of resolving the conflict for dependencies from different configurations. In this case, we need to explicitly mention the version of the library which we want to include in the final build for any one of the configurations.

This approach is a cleaner approach to resolve the conflicts, but the downside is that while updating the actual dependencies like junit and mockito, the developer needs to update the conflicted library as well.

// Depends on version 1.3 of org.hamcrest:hamcrest-core 
implementation 'junit:junit:4.12' 
// Depends on version 1.1 of org.hamcrest:hamcrest-core
androidTestImplementation 'org.mockito:mockito-core:1.10.19' 
// Explictly mention that include version 1.3 of org.hamcrest:hamcrest-core
androidTestCompile 'org.hamcrest:hamcrest-core:1.3' 

4: Force Resolution of the Library

This is another way of resolving the conflict in which instead of declaring for one configuration, we force it for all the configurations. In our example below, you can add your own "resolutionStrategy" into module level build.gradle, so it forces the specified version of package to be included regardless of dependencies in same or different configurations.

This approach should be used with some caution. In our first example, if the audience-network-sdk is updated and these libraries update the version of exoplayer-core and exoplayer-dash libraries, we would still be forcing to use a backward version. Although the scenario holds true for the second solution approach, in this approach we are forcing the dependency version on all the configurations instead on a single configuration.

Scenario 1: Dependencies in Same Configuration
android {
    configurations.all {
        resolutionStrategy.force 'com.google.android.exoplayer:exoplayer-core:r2.4.0'
        resolutionStrategy.force 'com.google.android.exoplayer:exoplayer-dash:r2.4.0'
    }
}
Scenario 2: Dependencies in Different Configuration
android {
    configurations.all {
        resolutionStrategy.force 'org.hamcrest:hamcrest-core:1.1'
    }
}

Next Steps

More Resources

Getting Started Guide

Technical guide to getting started with Audience Network

API Reference

Facebook SDK for iOS Reference